// Urho3D editor node transform gizmo handling

Node@ gizmoNode;
StaticModel@ gizmo;

const float axisMaxD = 0.1;
const float axisMaxT = 1.0;
const float rotSensitivity = 50.0;

EditMode lastGizmoMode;

// For undo
bool previousGizmoDrag;
bool needGizmoUndo;
Array<Transform> oldGizmoTransforms;

class GizmoAxis
{
    Ray axisRay;
    bool selected;
    bool lastSelected;
    float t;
    float d;
    float lastT;
    float lastD;

    GizmoAxis()
    {
        selected = false;
        lastSelected = false;
        t = 0.0;
        d = 0.0;
        lastT = 0.0;
        lastD = 0.0;
    }

    void Update(Ray cameraRay, float scale, bool drag)
    {
        // Do not select when UI has modal element
        if (ui.HasModalElement())
        {
            selected = false;
            return;
        }

        Vector3 closest = cameraRay.ClosestPoint(axisRay);
        Vector3 projected = axisRay.Project(closest);
        d = axisRay.Distance(closest);
        t = (projected - axisRay.origin).DotProduct(axisRay.direction);

        // Determine the sign of d from a plane that goes through the camera position to the axis
        Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction);
        if (axisPlane.Distance(closest) < 0.0)
            d = -d;

        // Update selected status only when not dragging
        if (!drag)
        {
            selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale;
            lastT = t;
            lastD = d;
        }
    }

    void Moved()
    {
        lastT = t;
        lastD = d;
    }
}

GizmoAxis gizmoAxisX;
GizmoAxis gizmoAxisY;
GizmoAxis gizmoAxisZ;

void CreateGizmo()
{
    gizmoNode = Node();
    gizmo = gizmoNode.CreateComponent("StaticModel");
    gizmo.model = cache.GetResource("Model", "Models/Axes.mdl");
    gizmo.materials[0] = cache.GetResource("Material", "Materials/RedUnlit.xml");
    gizmo.materials[1] = cache.GetResource("Material", "Materials/GreenUnlit.xml");
    gizmo.materials[2] = cache.GetResource("Material", "Materials/BlueUnlit.xml");
    gizmo.enabled = false;
    gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff
    gizmo.occludee = false;

    gizmoAxisX.lastSelected = false;
    gizmoAxisY.lastSelected = false;
    gizmoAxisZ.lastSelected = false;
    lastGizmoMode = EDIT_MOVE;
}

void HideGizmo()
{
    if (gizmo !is null)
        gizmo.enabled = false;
}

void ShowGizmo()
{
    if (gizmo !is null)
    {
        gizmo.enabled = true;

        // Because setting enabled = false detaches the gizmo from octree,
        // and it is a manually added drawable, must readd to octree when showing
        if (editorScene.octree !is null)
            editorScene.octree.AddManualDrawable(gizmo);
    }
}

void UpdateGizmo()
{
    UseGizmo();
    PositionGizmo();
    ResizeGizmo();
}

void PositionGizmo()
{
    if (gizmo is null)
        return;

    if (editNodes.empty)
    {
        HideGizmo();
        return;
    }

    Vector3 center(0, 0, 0);

    for (uint i = 0; i < editNodes.length; ++i)
        center += editNodes[i].worldPosition;

    center /= editNodes.length;
    gizmoNode.position = center;

    if (axisMode == AXIS_WORLD || editNodes.length > 1)
        gizmoNode.rotation = Quaternion();
    else
        gizmoNode.rotation = editNodes[0].worldRotation;

    if (editMode != lastGizmoMode)
    {
        switch (editMode)
        {
        case EDIT_MOVE:
            gizmo.model = cache.GetResource("Model", "Models/Axes.mdl");
            break;

        case EDIT_ROTATE:
            gizmo.model = cache.GetResource("Model", "Models/RotateAxes.mdl");
            break;

        case EDIT_SCALE:
            gizmo.model = cache.GetResource("Model", "Models/ScaleAxes.mdl");
            break;
        }

        lastGizmoMode = editMode;
    }

    if (editMode != EDIT_SELECT && !gizmo.enabled)
        ShowGizmo();
    else if (editMode == EDIT_SELECT && gizmo.enabled)
        HideGizmo();
}

void ResizeGizmo()
{
    if (gizmo is null || !gizmo.enabled)
        return;

    float c = 0.1;

    if (camera.orthographic)
        gizmoNode.scale = Vector3(c, c, c);
    else
    {
        /// \todo if matrix classes were exposed to script could simply use the camera's inverse world transform
        float z = (cameraNode.worldRotation.Inverse() * (gizmoNode.worldPosition - cameraNode.worldPosition)).z;
        gizmoNode.scale = Vector3(c * z, c * z, c * z);
    }
}

void CalculateGizmoAxes()
{
    gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0));
    gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0));
    gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1));
}

void GizmoMoved()
{
    gizmoAxisX.Moved();
    gizmoAxisY.Moved();
    gizmoAxisZ.Moved();
}

void UseGizmo()
{
    if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT)
    {
        StoreGizmoEditActions();
        previousGizmoDrag = false;
        return;
    }

    IntVector2 pos = ui.cursorPosition;
    if (ui.GetElementAt(pos) !is null)
        return;
    Ray cameraRay = camera.GetScreenRay(float(pos.x) / graphics.width, float(pos.y) / graphics.height);
    float scale = gizmoNode.scale.x;

    // Recalculate axes only when not left-dragging
    bool drag = input.mouseButtonDown[MOUSEB_LEFT];
    if (!drag)
        CalculateGizmoAxes();

    gizmoAxisX.Update(cameraRay, scale, drag);
    gizmoAxisY.Update(cameraRay, scale, drag);
    gizmoAxisZ.Update(cameraRay, scale, drag);

    if (gizmoAxisX.selected != gizmoAxisX.lastSelected)
    {
        gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Materials/BrightRedUnlit.xml" : "Materials/RedUnlit.xml");
        gizmoAxisX.lastSelected = gizmoAxisX.selected;
    }
    if (gizmoAxisY.selected != gizmoAxisY.lastSelected)
    {
        gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Materials/BrightGreenUnlit.xml" : "Materials/GreenUnlit.xml");
        gizmoAxisY.lastSelected = gizmoAxisY.selected;
    }
    if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected)
    {
        gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Materials/BrightBlueUnlit.xml" : "Materials/BlueUnlit.xml");
        gizmoAxisZ.lastSelected = gizmoAxisZ.selected;
    };

    if (drag)
    {
        // Store initial transforms for undo when gizmo drag started
        if (!previousGizmoDrag)
        {
            oldGizmoTransforms.Resize(editNodes.length);
            for (uint i = 0; i < editNodes.length; ++i)
                oldGizmoTransforms[i].Define(editNodes[i]);
        }

        bool moved = false;

        if (editMode == EDIT_MOVE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            moved = MoveNodes(adjust);
        }
        else if (editMode == EDIT_ROTATE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale;
            if (gizmoAxisY.selected)
                adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale;
            if (gizmoAxisZ.selected)
                adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale;

            moved = RotateNodes(adjust);
        }
        else if (editMode == EDIT_SCALE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            // Special handling for uniform scale: use the unmodified X-axis movement only
            if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected)
            {
                float x = gizmoAxisX.t - gizmoAxisX.lastT;
                adjust = Vector3(x, x, x);
            }

            moved = ScaleNodes(adjust);
        }

        if (moved)
        {
            GizmoMoved();
            UpdateNodeAttributes();
            needGizmoUndo = true;
        }
    }
    else
    {
        if (previousGizmoDrag)
            StoreGizmoEditActions();
    }

    previousGizmoDrag = drag;
}

bool IsGizmoSelected()
{
    return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected);
}

bool MoveNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            if (moveSnap)
            {
                float moveStepScaled = moveStep * snapScale;
                adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled;
                adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled;
                adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled;
            }

            Node@ node = editNodes[i];
            Vector3 nodeAdjust = adjust;
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                nodeAdjust = node.worldRotation * nodeAdjust;

            Vector3 worldPos = node.worldPosition;
            Vector3 oldPos = node.position;

            worldPos += nodeAdjust;

            if (node.parent is null)
                node.position = worldPos;
            else
                node.position = node.parent.WorldToLocal(worldPos);

            if (node.position != oldPos)
                moved = true;
        }
    }

    return moved;
}

bool RotateNodes(Vector3 adjust)
{
    bool moved = false;

    if (rotateSnap)
    {
        float rotateStepScaled = rotateStep * snapScale;
        adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled;
    }

    if (adjust.length > M_EPSILON)
    {
        moved = true;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];
            Quaternion rotQuat(adjust);
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                node.rotation = node.rotation * rotQuat;
            else
            {
                Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin;
                if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0))
                    rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation;
                node.rotation = rotQuat * node.rotation;
                Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset;
                if (node.parent !is null)
                    newPosition = node.parent.WorldToLocal(newPosition);
                node.position = newPosition;
            }
        }
    }

    return moved;
}

bool ScaleNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];

            Vector3 scale = node.scale;
            Vector3 oldScale = scale;

            if (!scaleSnap)
                scale += adjust;
            else
            {
                float scaleStepScaled = scaleStep * snapScale;
                if (adjust.x != 0)
                {
                    scale.x += adjust.x * scaleStepScaled;
                    scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.y != 0)
                {
                    scale.y += adjust.y * scaleStepScaled;
                    scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.z != 0)
                {
                    scale.z += adjust.z * scaleStepScaled;
                    scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled;
                }
            }

            if (scale != oldScale)
                moved = true;

            node.scale = scale;
        }
    }

    return moved;
}

void StoreGizmoEditActions()
{
    if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length)
    {
        EditActionGroup group;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            EditNodeTransformAction action;
            action.Define(editNodes[i], oldGizmoTransforms[i]);
            group.actions.Push(action);
        }

        SaveEditActionGroup(group);
        SetSceneModified();
    }

    needGizmoUndo = false;
}

// Urho3D editor node transform gizmo handling

Node@ gizmoNode;
StaticModel@ gizmo;

const float axisMaxD = 0.1;
const float axisMaxT = 1.0;
const float rotSensitivity = 50.0;

EditMode lastGizmoMode;

// For undo
bool previousGizmoDrag;
bool needGizmoUndo;
Array<Transform> oldGizmoTransforms;

class GizmoAxis
{
    Ray axisRay;
    bool selected;
    bool lastSelected;
    float t;
    float d;
    float lastT;
    float lastD;

    GizmoAxis()
    {
        selected = false;
        lastSelected = false;
        t = 0.0;
        d = 0.0;
        lastT = 0.0;
        lastD = 0.0;
    }

    void Update(Ray cameraRay, float scale, bool drag)
    {
        // Do not select when UI has modal element
        if (ui.HasModalElement())
        {
            selected = false;
            return;
        }

        Vector3 closest = cameraRay.ClosestPoint(axisRay);
        Vector3 projected = axisRay.Project(closest);
        d = axisRay.Distance(closest);
        t = (projected - axisRay.origin).DotProduct(axisRay.direction);

        // Determine the sign of d from a plane that goes through the camera position to the axis
        Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction);
        if (axisPlane.Distance(closest) < 0.0)
            d = -d;

        // Update selected status only when not dragging
        if (!drag)
        {
            selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale;
            lastT = t;
            lastD = d;
        }
    }

    void Moved()
    {
        lastT = t;
        lastD = d;
    }
}

GizmoAxis gizmoAxisX;
GizmoAxis gizmoAxisY;
GizmoAxis gizmoAxisZ;

void CreateGizmo()
{
    gizmoNode = Node();
    gizmo = gizmoNode.CreateComponent("StaticModel");
    gizmo.model = cache.GetResource("Model", "Models/Axes.mdl");
    gizmo.materials[0] = cache.GetResource("Material", "Materials/RedUnlit.xml");
    gizmo.materials[1] = cache.GetResource("Material", "Materials/GreenUnlit.xml");
    gizmo.materials[2] = cache.GetResource("Material", "Materials/BlueUnlit.xml");
    gizmo.enabled = false;
    gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff
    gizmo.occludee = false;

    gizmoAxisX.lastSelected = false;
    gizmoAxisY.lastSelected = false;
    gizmoAxisZ.lastSelected = false;
    lastGizmoMode = EDIT_MOVE;
}

void HideGizmo()
{
    if (gizmo !is null)
        gizmo.enabled = false;
}

void ShowGizmo()
{
    if (gizmo !is null)
    {
        gizmo.enabled = true;

        // Because setting enabled = false detaches the gizmo from octree,
        // and it is a manually added drawable, must readd to octree when showing
        if (editorScene.octree !is null)
            editorScene.octree.AddManualDrawable(gizmo);
    }
}

void UpdateGizmo()
{
    UseGizmo();
    PositionGizmo();
    ResizeGizmo();
}

void PositionGizmo()
{
    if (gizmo is null)
        return;

    if (editNodes.empty)
    {
        HideGizmo();
        return;
    }

    Vector3 center(0, 0, 0);

    for (uint i = 0; i < editNodes.length; ++i)
        center += editNodes[i].worldPosition;

    center /= editNodes.length;
    gizmoNode.position = center;

    if (axisMode == AXIS_WORLD || editNodes.length > 1)
        gizmoNode.rotation = Quaternion();
    else
        gizmoNode.rotation = editNodes[0].worldRotation;

    if (editMode != lastGizmoMode)
    {
        switch (editMode)
        {
        case EDIT_MOVE:
            gizmo.model = cache.GetResource("Model", "Models/Axes.mdl");
            break;

        case EDIT_ROTATE:
            gizmo.model = cache.GetResource("Model", "Models/RotateAxes.mdl");
            break;

        case EDIT_SCALE:
            gizmo.model = cache.GetResource("Model", "Models/ScaleAxes.mdl");
            break;
        }

        lastGizmoMode = editMode;
    }

    if ((editMode != EDIT_SELECT && !orbiting) && !gizmo.enabled)
        ShowGizmo();
    else if ((editMode == EDIT_SELECT || orbiting) && gizmo.enabled)
        HideGizmo();
}

void ResizeGizmo()
{
    if (gizmo is null || !gizmo.enabled)
        return;

    float c = 0.1;

    if (camera.orthographic)
        gizmoNode.scale = Vector3(c, c, c);
    else
    {
        /// \todo if matrix classes were exposed to script could simply use the camera's inverse world transform
        float z = (cameraNode.worldRotation.Inverse() * (gizmoNode.worldPosition - cameraNode.worldPosition)).z;
        gizmoNode.scale = Vector3(c * z, c * z, c * z);
    }
}

void CalculateGizmoAxes()
{
    gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0));
    gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0));
    gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1));
}

void GizmoMoved()
{
    gizmoAxisX.Moved();
    gizmoAxisY.Moved();
    gizmoAxisZ.Moved();
}

void UseGizmo()
{
    if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT)
    {
        StoreGizmoEditActions();
        previousGizmoDrag = false;
        return;
    }

    IntVector2 pos = ui.cursorPosition;
    if (ui.GetElementAt(pos) !is null)
        return;
    Ray cameraRay = camera.GetScreenRay(float(pos.x) / graphics.width, float(pos.y) / graphics.height);
    float scale = gizmoNode.scale.x;

    // Recalculate axes only when not left-dragging
    bool drag = input.mouseButtonDown[MOUSEB_LEFT];
    if (!drag)
        CalculateGizmoAxes();

    gizmoAxisX.Update(cameraRay, scale, drag);
    gizmoAxisY.Update(cameraRay, scale, drag);
    gizmoAxisZ.Update(cameraRay, scale, drag);

    if (gizmoAxisX.selected != gizmoAxisX.lastSelected)
    {
        gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Materials/BrightRedUnlit.xml" : "Materials/RedUnlit.xml");
        gizmoAxisX.lastSelected = gizmoAxisX.selected;
    }
    if (gizmoAxisY.selected != gizmoAxisY.lastSelected)
    {
        gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Materials/BrightGreenUnlit.xml" : "Materials/GreenUnlit.xml");
        gizmoAxisY.lastSelected = gizmoAxisY.selected;
    }
    if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected)
    {
        gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Materials/BrightBlueUnlit.xml" : "Materials/BlueUnlit.xml");
        gizmoAxisZ.lastSelected = gizmoAxisZ.selected;
    };

    if (drag)
    {
        // Store initial transforms for undo when gizmo drag started
        if (!previousGizmoDrag)
        {
            oldGizmoTransforms.Resize(editNodes.length);
            for (uint i = 0; i < editNodes.length; ++i)
                oldGizmoTransforms[i].Define(editNodes[i]);
        }

        bool moved = false;

        if (editMode == EDIT_MOVE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            moved = MoveNodes(adjust);
        }
        else if (editMode == EDIT_ROTATE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale;
            if (gizmoAxisY.selected)
                adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale;
            if (gizmoAxisZ.selected)
                adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale;

            moved = RotateNodes(adjust);
        }
        else if (editMode == EDIT_SCALE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            // Special handling for uniform scale: use the unmodified X-axis movement only
            if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected)
            {
                float x = gizmoAxisX.t - gizmoAxisX.lastT;
                adjust = Vector3(x, x, x);
            }

            moved = ScaleNodes(adjust);
        }

        if (moved)
        {
            GizmoMoved();
            UpdateNodeAttributes();
            needGizmoUndo = true;
        }
    }
    else
    {
        if (previousGizmoDrag)
            StoreGizmoEditActions();
    }

    previousGizmoDrag = drag;
}

bool IsGizmoSelected()
{
    return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected);
}

bool MoveNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            if (moveSnap)
            {
                float moveStepScaled = moveStep * snapScale;
                adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled;
                adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled;
                adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled;
            }

            Node@ node = editNodes[i];
            Vector3 nodeAdjust = adjust;
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                nodeAdjust = node.worldRotation * nodeAdjust;

            Vector3 worldPos = node.worldPosition;
            Vector3 oldPos = node.position;

            worldPos += nodeAdjust;

            if (node.parent is null)
                node.position = worldPos;
            else
                node.position = node.parent.WorldToLocal(worldPos);

            if (node.position != oldPos)
                moved = true;
        }
    }

    return moved;
}

bool RotateNodes(Vector3 adjust)
{
    bool moved = false;

    if (rotateSnap)
    {
        float rotateStepScaled = rotateStep * snapScale;
        adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled;
    }

    if (adjust.length > M_EPSILON)
    {
        moved = true;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];
            Quaternion rotQuat(adjust);
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                node.rotation = node.rotation * rotQuat;
            else
            {
                Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin;
                if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0))
                    rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation;
                node.rotation = rotQuat * node.rotation;
                Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset;
                if (node.parent !is null)
                    newPosition = node.parent.WorldToLocal(newPosition);
                node.position = newPosition;
            }
        }
    }

    return moved;
}

bool ScaleNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];

            Vector3 scale = node.scale;
            Vector3 oldScale = scale;

            if (!scaleSnap)
                scale += adjust;
            else
            {
                float scaleStepScaled = scaleStep * snapScale;
                if (adjust.x != 0)
                {
                    scale.x += adjust.x * scaleStepScaled;
                    scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.y != 0)
                {
                    scale.y += adjust.y * scaleStepScaled;
                    scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.z != 0)
                {
                    scale.z += adjust.z * scaleStepScaled;
                    scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled;
                }
            }

            if (scale != oldScale)
                moved = true;

            node.scale = scale;
        }
    }

    return moved;
}

void StoreGizmoEditActions()
{
    if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length)
    {
        EditActionGroup group;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            EditNodeTransformAction action;
            action.Define(editNodes[i], oldGizmoTransforms[i]);
            group.actions.Push(action);
        }

        SaveEditActionGroup(group);
        SetSceneModified();
    }

    needGizmoUndo = false;
}

Hide also the gizmo when orbiting.

Hide also the gizmo when orbiting.


Scene@ newScene_;
Scene@ scene_;
Camera@ camera_;
Timer timer_;

void Start()
{
	StartScene("Scenes/Level1.xml");
	SubscribeToEvent("LevelComplete", "HandleLevelComplete");
}

void Stop()
{
}

void HandleLevelComplete(StringHash type, VariantMap& data)
{
	log.Debug("Level Complete. I should be loading "+data["NextLevel"].GetString());
	float timeTaken = timer_.GetMSec(false);
	log.Debug("You took " + timeTaken / 1000.f + " seconds to solve the level");

	StartScene(data["NextLevel"].GetString());
}

void StartScene(String scene)
{
	newScene_ = Scene();
	newScene_.LoadAsyncXML(cache.GetFile(scene));

	SubscribeToEvent("AsyncLoadFinished", "HandleAsyncLoadFinished");
}

void HandleAsyncLoadFinished(StringHash type, VariantMap& data)
{
	UnsubscribeFromEvent("AsyncLoadFinished");

	newScene_ = data["Scene"].GetPtr();

	SubscribeToEvent("Update", "HandleDelayedStart");
}

void HandleDelayedStart(StringHash type, VariantMap& data)
{
	UnsubscribeFromEvent("Update");

	Node@ cameraNode = newScene_.GetChild("Camera", true);
	Camera@ newCamera = cameraNode.CreateComponent("Camera");

	Viewport@ viewport = Viewport(newScene_, cameraNode.GetComponent("Camera"));
	renderer.viewports[0] = viewport;

	scene_ = newScene_;
	camera_ = newCamera;
	newScene_ = null;

	timer_.Reset();
}

#include "Scripts/PlayerPower.as"

Scene@ newScene_;
Scene@ scene_;
Camera@ camera_;
Timer timer_;

void Start()
{
	StartScene("Scenes/Level1.xml");
	SubscribeToEvent("LevelComplete", "HandleLevelComplete");
}

void Stop()
{
}

void HandleLevelComplete(StringHash type, VariantMap& data)
{
	log.Debug("Level Complete. I should be loading "+data["NextLevel"].GetString());
	float timeTaken = timer_.GetMSec(false);
	log.Debug("You took " + timeTaken / 1000.f + " seconds to solve the level");

	Node@[] playerNode = scene_.GetChildrenWithTag("player", true);
	PlayerPower@ playerPower = cast<PlayerPower>(playerNode[0].GetScriptObject("PlayerPower"));
	float power = playerPower.Power;
	log.Debug("You had " + power + " power remaining");

	StartScene(data["NextLevel"].GetString());
}

void StartScene(String scene)
{
	newScene_ = Scene();
	newScene_.LoadAsyncXML(cache.GetFile(scene));

	SubscribeToEvent("AsyncLoadFinished", "HandleAsyncLoadFinished");
}

void HandleAsyncLoadFinished(StringHash type, VariantMap& data)
{
	UnsubscribeFromEvent("AsyncLoadFinished");

	newScene_ = data["Scene"].GetPtr();

	SubscribeToEvent("Update", "HandleDelayedStart");
}

void HandleDelayedStart(StringHash type, VariantMap& data)
{
	UnsubscribeFromEvent("Update");

	Node@ cameraNode = newScene_.GetChild("Camera", true);
	Camera@ newCamera = cameraNode.CreateComponent("Camera");

	Viewport@ viewport = Viewport(newScene_, cameraNode.GetComponent("Camera"));
	renderer.viewports[0] = viewport;

	scene_ = newScene_;
	camera_ = newCamera;
	newScene_ = null;

	timer_.Reset();
}

Determine player power at the end of each level.


/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mangui.flowplayer {
    import org.mangui.hls.event.HLSEvent;
    import org.mangui.hls.utils.Params2Settings;

    import flash.display.DisplayObject;
    import flash.net.NetConnection;
    import flash.net.NetStream;
    import flash.utils.Dictionary;
    import flash.media.Video;

    import org.mangui.hls.HLS;
    import org.mangui.hls.constant.HLSPlayStates;

    import org.flowplayer.model.Plugin;
    import org.flowplayer.model.PluginModel;
    import org.flowplayer.view.Flowplayer;
    import org.flowplayer.controller.StreamProvider;
    import org.flowplayer.controller.TimeProvider;
    import org.flowplayer.controller.VolumeController;
    import org.flowplayer.model.Clip;
    import org.flowplayer.model.ClipType;
    import org.flowplayer.model.ClipEvent;
    import org.flowplayer.model.ClipEventType;
    import org.flowplayer.model.Playlist;
    import org.flowplayer.view.StageVideoWrapper;

    CONFIG::LOGGING {
    import org.mangui.hls.utils.Log;
    }

    public class HLSStreamProvider  implements StreamProvider,Plugin {
        private var _volumecontroller : VolumeController;
        private var _playlist : Playlist;
        private var _timeProvider : TimeProvider;
        private var _model : PluginModel;
        private var _player : Flowplayer;
        private var _clip : Clip;
        private var _video : Video;
        /** reference to the framework. **/
        private var _hls : HLS;
        // event values
        private var _position : Number = 0;
        private var _duration : Number = 0;
        private var _bufferedTime : Number = 0;
        private var _videoWidth : int = -1;
        private var _videoHeight : int = -1;
        private var _isManifestLoaded : Boolean = false;
        private var _pauseAfterStart : Boolean;
        private var _seekable : Boolean = false;

        public function getDefaultConfig() : Object {
            return null;
        }

        public function onConfig(model : PluginModel) : void {
            CONFIG::LOGGING {
            Log.info("onConfig()");
            }
            _model = model;
        }

        public function onLoad(player : Flowplayer) : void {
            CONFIG::LOGGING {
            Log.info("onLoad()");
            }
            _player = player;
            _hls = new HLS();
            _hls.stage = player.screen.getDisplayObject().stage;
            _hls.addEventListener(HLSEvent.PLAYBACK_COMPLETE, _completeHandler);
            _hls.addEventListener(HLSEvent.ERROR, _errorHandler);
            _hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestHandler);
            _hls.addEventListener(HLSEvent.MEDIA_TIME, _mediaTimeHandler);
            _hls.addEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
            _hls.addEventListener(HLSEvent.ID3_UPDATED, _ID3Handler);

            var cfg : Object = _model.config;
            for (var object : String in cfg) {
                var subidx : int = object.indexOf("hls_");
                if (subidx != -1) {
                    Params2Settings.set(object.substr(4), cfg[object]);
                }
            }

            _model.dispatchOnLoad();
        }

        private function _completeHandler(event : HLSEvent) : void {
            // dispatch a before event because the finish has default behavior that can be prevented by listeners
            _clip.dispatchBeforeEvent(new ClipEvent(ClipEventType.FINISH));
        };

        private function _errorHandler(event : HLSEvent) : void {
        };

        private function _ID3Handler(event : HLSEvent) : void {
            _clip.dispatch(ClipEventType.NETSTREAM_EVENT, "onID3", event.ID3Data);
        };

        private function _manifestHandler(event : HLSEvent) : void {
            _duration = event.levels[_hls.startlevel].duration;
            _isManifestLoaded = true;
            _clip.duration = _duration;
            _clip.stopLiveOnPause = false;
            _clip.dispatch(ClipEventType.METADATA);
            _seekable = true;
            // if (_hls.type == HLSTypes.LIVE) {
            // _seekable = false;
            // } else {
            // _seekable = true;
            // }
            _hls.stream.play();
            _clip.dispatch(ClipEventType.SEEK);
            if (_pauseAfterStart) {
                pause(new ClipEvent(ClipEventType.PAUSE));
            }
        };

        private function _mediaTimeHandler(event : HLSEvent) : void {
            _position = Math.max(0, event.mediatime.position);
            _duration = event.mediatime.duration;
            _clip.duration = _duration;
            _bufferedTime = event.mediatime.buffer + event.mediatime.position;
            var videoWidth : int = _video.videoWidth;
            var videoHeight : int = _video.videoHeight;
            if (videoWidth && videoHeight) {
                var changed : Boolean = _videoWidth != videoWidth || _videoHeight != videoHeight;
                if (changed) {
                    CONFIG::LOGGING {
                    Log.info("video size changed to " + videoWidth + "/" + videoHeight);
                    }
                    _videoWidth = videoWidth;
                    _videoHeight = videoHeight;
                    _clip.originalWidth = videoWidth;
                    _clip.originalHeight = videoHeight;
                    _clip.dispatch(ClipEventType.START);
                    _clip.dispatch(ClipEventType.METADATA_CHANGED);
                }
            }
        };

        private function _stateHandler(event : HLSEvent) : void {
            // CONFIG::LOGGING {
            // Log.txt("state:"+ event.state);
            // }
            switch(event.state) {
                case HLSPlayStates.IDLE:
                case HLSPlayStates.PLAYING:
                case HLSPlayStates.PAUSED:
                    _clip.dispatch(ClipEventType.BUFFER_FULL);
                    break;
                case HLSPlayStates.PLAYING_BUFFERING:
                case HLSPlayStates.PAUSED_BUFFERING:
                    _clip.dispatch(ClipEventType.BUFFER_EMPTY);
                    break;
                default:
                    break;
            }
        };

        /**
         * Starts loading the specified clip. Once video data is available the provider
         * must set it to the clip using clip.setContent(). Typically the video
         * object passed to the clip is an instance of flash.media.Video.
         *
         * @param event the event that this provider should dispatch once loading has successfully started,
         * once dispatched the player will call getVideo()
         * @param clip the clip to load
         * @param pauseAfterStart if true the playback is paused on first frame and
         * buffering is continued
         * @see Clip#setContent()
         * @see #getVideo()
         */
        public function load(event : ClipEvent, clip : Clip, pauseAfterStart : Boolean = true) : void {
            _clip = clip;
            CONFIG::LOGGING {
            Log.info("load()" + clip.completeUrl);
            }
            _hls.load(clip.completeUrl);
            _pauseAfterStart = pauseAfterStart;
            clip.type = ClipType.VIDEO;
            clip.dispatch(ClipEventType.BEGIN);
            clip.setNetStream(_hls.stream);
            return;
        }

        /**
         * Gets the Video object.
         * A stream will be attached to the returned video object using attachStream().
         * @param clip the clip for which the Video object is queried for
         * @see #attachStream()
         */
        public function getVideo(clip : Clip) : DisplayObject {
            CONFIG::LOGGING {
            Log.debug("getVideo()");
            }
            if (_video == null) {
                if (clip.useStageVideo) {
                    CONFIG::LOGGING {
                    Log.debug("useStageVideo");
                    }
                    _video = new StageVideoWrapper(clip);
                } else {
                    _video = new Video();
                    _video.smoothing = clip.smoothing;
                }
            }
            return _video;
        }

        /**
         * Attaches a stream to the specified display object.
         * @param video the video object that was originally retrieved using getVideo().
         * @see #getVideo()
         */
        public function attachStream(video : DisplayObject) : void {
            CONFIG::LOGGING {
            Log.debug("attachStream()");
            }
            Video(video).attachNetStream(_hls.stream);
            return;
        }

        /**
         * Pauses playback.
         * @param event the event that this provider should dispatch once loading has been successfully paused
         */
        public function pause(event : ClipEvent) : void {
            CONFIG::LOGGING {
            Log.info("pause()");
            }
            _hls.stream.pause();
            if (event) {
                _clip.dispatch(ClipEventType.PAUSE);
            }
            return;
        }

        /**
         * Resumes playback.
         * @param event the event that this provider should dispatch once loading has been successfully resumed
         */
        public function resume(event : ClipEvent) : void {
            CONFIG::LOGGING {
            Log.info("resume()");
            }
            _hls.stream.resume();
            _clip.dispatch(ClipEventType.RESUME);
            return;
        }

        /**
         * Stops and rewinds to the beginning of current clip.
         * @param event the event that this provider should dispatch once loading has been successfully stopped
         */
        public function stop(event : ClipEvent, closeStream : Boolean = false) : void {
            CONFIG::LOGGING {
            Log.info("stop()");
            }
            _hls.stream.close();
            return;
        }

        /**
         * Seeks to the specified point in the timeline.
         * @param event the event that this provider should dispatch once the seek is in target
         * @param seconds the target point in the timeline
         */
        public function seek(event : ClipEvent, seconds : Number) : void {
            CONFIG::LOGGING {
            Log.info("seek()");
            }
            _hls.stream.seek(seconds);
            _position = seconds;
            _bufferedTime = seconds;
            _clip.dispatch(ClipEventType.SEEK, seconds);
            return;
        }

        /**
         * File size in bytes.
         */
        public function get fileSize() : Number {
            return 0;
        }

        /**
         * Current playhead time in seconds.
         */
        public function get time() : Number {
            return _position;
        }

        /**
         * The point in timeline where the buffered data region begins, in seconds.
         */
        public function get bufferStart() : Number {
            return 0;
        }

        /**
         * The point in timeline where the buffered data region ends, in seconds.
         */
        public function get bufferEnd() : Number {
            return _bufferedTime;
        }

        /**
         * Does this provider support random seeking to unbuffered areas in the timeline?
         */
        public function get allowRandomSeek() : Boolean {
            // CONFIG::LOGGING {
            // Log.info("allowRandomSeek()");
            // }
            return _seekable;
        }

        /**
         * Volume controller used to control the video volume.
         */
        public function set volumeController(controller : VolumeController) : void {
            _volumecontroller = controller;
            _volumecontroller.netStream = _hls.stream;
            return;
        }

        /**
         * Is this provider in the process of stopping the stream?
         * When stopped the provider should not dispatch any events resulting from events that
         * might get triggered by the underlying streaming implementation.
         */
        public function get stopping() : Boolean {
            CONFIG::LOGGING {
            Log.info("stopping()");
            }
            return false;
        }

        /**
         * The playlist instance.
         */
        public function set playlist(playlist : Playlist) : void {
            // CONFIG::LOGGING {
            // Log.debug("set playlist()");
            // }
            _playlist = playlist;
            return;
        }

        public function get playlist() : Playlist {
            CONFIG::LOGGING {
            Log.debug("get playlist()");
            }
            return _playlist;
        }

        /**
         * Adds a callback public function to the NetConnection instance. This public function will fire ClipEvents whenever
         * the callback is invoked in the connection.
         * @param name
         * @param listener
         * @return
         * @see ClipEventType#CONNECTION_EVENT
         */
        public function addConnectionCallback(name : String, listener : Function) : void {
            CONFIG::LOGGING {
            Log.debug("addConnectionCallback()");
            }
            return;
        }

        /**
         * Adds a callback public function to the NetStream object. This public function will fire a ClipEvent of type StreamEvent whenever
         * the callback has been invoked on the stream. The invokations typically come from a server-side app running
         * on RTMP server.
         * @param name
         * @param listener
         * @return
         * @see ClipEventType.NETSTREAM_EVENT
         */
        public function addStreamCallback(name : String, listener : Function) : void {
            CONFIG::LOGGING {
            Log.debug("addStreamCallback()");
            }
            return;
        }

        /**
         * Get the current stream callbacks.
         * @return a dictionary of callbacks, keyed using callback names and values being the callback functions
         */
        public function get streamCallbacks() : Dictionary {
            CONFIG::LOGGING {
            Log.debug("get streamCallbacks()");
            }
            return null;
        }

        /**
         * Gets the underlying NetStream object.
         * @return the netStream currently in use, or null if this provider has not started streaming yet
         */
        public function get netStream() : NetStream {
            CONFIG::LOGGING {
            Log.debug("get netStream()");
            }
            return _hls.stream;
        }

        /**
         * Gets the underlying netConnection object.
         * @return the netConnection currently in use, or null if this provider has not started streaming yet
         */
        public function get netConnection() : NetConnection {
            CONFIG::LOGGING {
            Log.debug("get netConnection()");
            }
            return null;
        }

        /**
         * Sets a time provider to be used by this StreamProvider. Normally the playhead time is queried from
         * the NetStream.time property.
         *
         * @param timeProvider
         */
        public function set timeProvider(timeProvider : TimeProvider) : void {
            CONFIG::LOGGING {
            Log.debug("set timeProvider()");
            }
            _timeProvider = timeProvider;
            return;
        }

        /**
         * Gets the type of StreamProvider either http, rtmp, psuedo.
         */
        public function get type() : String {
            return "httpstreaming";
        }

        /**
         * Switch the stream in realtime with / without dynamic stream switching support
         *
         * @param event ClipEvent the clip event
         * @param clip Clip the clip to switch to
         * @param netStreamPlayOptions Object the NetStreamPlayOptions object to enable dynamic stream switching
         */
        public function switchStream(event : ClipEvent, clip : Clip, netStreamPlayOptions : Object = null) : void {
            CONFIG::LOGGING {
            Log.info("switchStream()");
            }
            return;
        }
    }
}

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mangui.flowplayer {
    import org.mangui.hls.event.HLSEvent;
    import org.mangui.hls.utils.Params2Settings;

    import flash.display.DisplayObject;
    import flash.net.NetConnection;
    import flash.net.NetStream;
    import flash.utils.Dictionary;
    import flash.media.Video;

    import org.mangui.hls.HLS;
    import org.mangui.hls.constant.HLSPlayStates;

    import org.flowplayer.model.Plugin;
    import org.flowplayer.model.PluginModel;
    import org.flowplayer.view.Flowplayer;
    import org.flowplayer.controller.StreamProvider;
    import org.flowplayer.controller.TimeProvider;
    import org.flowplayer.controller.VolumeController;
    import org.flowplayer.model.Clip;
    import org.flowplayer.model.ClipType;
    import org.flowplayer.model.ClipEvent;
    import org.flowplayer.model.ClipEventType;
    import org.flowplayer.model.Playlist;
    import org.flowplayer.view.StageVideoWrapper;

    CONFIG::LOGGING {
    import org.mangui.hls.utils.Log;
    }

    public class HLSStreamProvider  implements StreamProvider,Plugin {
        private var _volumecontroller : VolumeController;
        private var _playlist : Playlist;
        private var _timeProvider : TimeProvider;
        private var _model : PluginModel;
        private var _player : Flowplayer;
        private var _clip : Clip;
        private var _video : Video;
        /** reference to the framework. **/
        private var _hls : HLS;
        // event values
        private var _position : Number = 0;
        private var _duration : Number = 0;
        private var _bufferedTime : Number = 0;
        private var _videoWidth : int = -1;
        private var _videoHeight : int = -1;
        private var _isManifestLoaded : Boolean = false;
        private var _pauseAfterStart : Boolean;
        private var _seekable : Boolean = false;

        public function getDefaultConfig() : Object {
            return null;
        }

        public function onConfig(model : PluginModel) : void {
            CONFIG::LOGGING {
            Log.info("onConfig()");
            }
            _model = model;
        }

        public function onLoad(player : Flowplayer) : void {
            CONFIG::LOGGING {
            Log.info("onLoad()");
            }
            _player = player;
            _hls = new HLS();
            _hls.stage = player.screen.getDisplayObject().stage;
            _hls.addEventListener(HLSEvent.PLAYBACK_COMPLETE, _completeHandler);
            _hls.addEventListener(HLSEvent.ERROR, _errorHandler);
            _hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestHandler);
            _hls.addEventListener(HLSEvent.MEDIA_TIME, _mediaTimeHandler);
            _hls.addEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
            _hls.addEventListener(HLSEvent.ID3_UPDATED, _ID3Handler);

            var cfg : Object = _model.config;
            for (var object : String in cfg) {
                var subidx : int = object.indexOf("hls_");
                if (subidx != -1) {
                    Params2Settings.set(object.substr(4), cfg[object]);
                }
            }

            _model.dispatchOnLoad();
        }

        private function _completeHandler(event : HLSEvent) : void {
            // dispatch a before event because the finish has default behavior that can be prevented by listeners
            _clip.dispatchBeforeEvent(new ClipEvent(ClipEventType.FINISH));
        };

        private function _errorHandler(event : HLSEvent) : void {
        };

        private function _ID3Handler(event : HLSEvent) : void {
            _clip.dispatch(ClipEventType.NETSTREAM_EVENT, "onID3", event.ID3Data);
        };

        private function _manifestHandler(event : HLSEvent) : void {
            _duration = event.levels[_hls.startlevel].duration;
            _isManifestLoaded = true;
            _clip.duration = _duration;
            _clip.stopLiveOnPause = false;
            _clip.dispatch(ClipEventType.METADATA);
            _seekable = true;
            // if (_hls.type == HLSTypes.LIVE) {
            // _seekable = false;
            // } else {
            // _seekable = true;
            // }
            _hls.stream.play();
            _clip.dispatch(ClipEventType.SEEK,0);
            if (_pauseAfterStart) {
                pause(new ClipEvent(ClipEventType.PAUSE));
            }
        };

        private function _mediaTimeHandler(event : HLSEvent) : void {
            _position = Math.max(0, event.mediatime.position);
            _duration = event.mediatime.duration;
            _clip.duration = _duration;
            _bufferedTime = event.mediatime.buffer + event.mediatime.position;
            var videoWidth : int = _video.videoWidth;
            var videoHeight : int = _video.videoHeight;
            if (videoWidth && videoHeight) {
                var changed : Boolean = _videoWidth != videoWidth || _videoHeight != videoHeight;
                if (changed) {
                    CONFIG::LOGGING {
                    Log.info("video size changed to " + videoWidth + "/" + videoHeight);
                    }
                    _videoWidth = videoWidth;
                    _videoHeight = videoHeight;
                    _clip.originalWidth = videoWidth;
                    _clip.originalHeight = videoHeight;
                    _clip.dispatch(ClipEventType.START);
                    _clip.dispatch(ClipEventType.METADATA_CHANGED);
                }
            }
        };

        private function _stateHandler(event : HLSEvent) : void {
            // CONFIG::LOGGING {
            // Log.txt("state:"+ event.state);
            // }
            switch(event.state) {
                case HLSPlayStates.IDLE:
                case HLSPlayStates.PLAYING:
                case HLSPlayStates.PAUSED:
                    _clip.dispatch(ClipEventType.BUFFER_FULL);
                    break;
                case HLSPlayStates.PLAYING_BUFFERING:
                case HLSPlayStates.PAUSED_BUFFERING:
                    _clip.dispatch(ClipEventType.BUFFER_EMPTY);
                    break;
                default:
                    break;
            }
        };

        /**
         * Starts loading the specified clip. Once video data is available the provider
         * must set it to the clip using clip.setContent(). Typically the video
         * object passed to the clip is an instance of flash.media.Video.
         *
         * @param event the event that this provider should dispatch once loading has successfully started,
         * once dispatched the player will call getVideo()
         * @param clip the clip to load
         * @param pauseAfterStart if true the playback is paused on first frame and
         * buffering is continued
         * @see Clip#setContent()
         * @see #getVideo()
         */
        public function load(event : ClipEvent, clip : Clip, pauseAfterStart : Boolean = true) : void {
            _clip = clip;
            CONFIG::LOGGING {
            Log.info("load()" + clip.completeUrl);
            }
            _hls.load(clip.completeUrl);
            _pauseAfterStart = pauseAfterStart;
            clip.type = ClipType.VIDEO;
            clip.dispatch(ClipEventType.BEGIN);
            clip.setNetStream(_hls.stream);
            return;
        }

        /**
         * Gets the Video object.
         * A stream will be attached to the returned video object using attachStream().
         * @param clip the clip for which the Video object is queried for
         * @see #attachStream()
         */
        public function getVideo(clip : Clip) : DisplayObject {
            CONFIG::LOGGING {
            Log.debug("getVideo()");
            }
            if (_video == null) {
                if (clip.useStageVideo) {
                    CONFIG::LOGGING {
                    Log.debug("useStageVideo");
                    }
                    _video = new StageVideoWrapper(clip);
                } else {
                    _video = new Video();
                    _video.smoothing = clip.smoothing;
                }
            }
            return _video;
        }

        /**
         * Attaches a stream to the specified display object.
         * @param video the video object that was originally retrieved using getVideo().
         * @see #getVideo()
         */
        public function attachStream(video : DisplayObject) : void {
            CONFIG::LOGGING {
            Log.debug("attachStream()");
            }
            Video(video).attachNetStream(_hls.stream);
            return;
        }

        /**
         * Pauses playback.
         * @param event the event that this provider should dispatch once loading has been successfully paused
         */
        public function pause(event : ClipEvent) : void {
            CONFIG::LOGGING {
            Log.info("pause()");
            }
            _hls.stream.pause();
            if (event) {
                _clip.dispatch(ClipEventType.PAUSE);
            }
            return;
        }

        /**
         * Resumes playback.
         * @param event the event that this provider should dispatch once loading has been successfully resumed
         */
        public function resume(event : ClipEvent) : void {
            CONFIG::LOGGING {
            Log.info("resume()");
            }
            _hls.stream.resume();
            _clip.dispatch(ClipEventType.RESUME);
            return;
        }

        /**
         * Stops and rewinds to the beginning of current clip.
         * @param event the event that this provider should dispatch once loading has been successfully stopped
         */
        public function stop(event : ClipEvent, closeStream : Boolean = false) : void {
            CONFIG::LOGGING {
            Log.info("stop()");
            }
            _hls.stream.close();
            return;
        }

        /**
         * Seeks to the specified point in the timeline.
         * @param event the event that this provider should dispatch once the seek is in target
         * @param seconds the target point in the timeline
         */
        public function seek(event : ClipEvent, seconds : Number) : void {
            CONFIG::LOGGING {
            Log.info("seek()");
            }
            _hls.stream.seek(seconds);
            _position = seconds;
            _bufferedTime = seconds;
            _clip.dispatch(ClipEventType.SEEK, seconds);
            return;
        }

        /**
         * File size in bytes.
         */
        public function get fileSize() : Number {
            return 0;
        }

        /**
         * Current playhead time in seconds.
         */
        public function get time() : Number {
            return _position;
        }

        /**
         * The point in timeline where the buffered data region begins, in seconds.
         */
        public function get bufferStart() : Number {
            return 0;
        }

        /**
         * The point in timeline where the buffered data region ends, in seconds.
         */
        public function get bufferEnd() : Number {
            return _bufferedTime;
        }

        /**
         * Does this provider support random seeking to unbuffered areas in the timeline?
         */
        public function get allowRandomSeek() : Boolean {
            // CONFIG::LOGGING {
            // Log.info("allowRandomSeek()");
            // }
            return _seekable;
        }

        /**
         * Volume controller used to control the video volume.
         */
        public function set volumeController(controller : VolumeController) : void {
            _volumecontroller = controller;
            _volumecontroller.netStream = _hls.stream;
            return;
        }

        /**
         * Is this provider in the process of stopping the stream?
         * When stopped the provider should not dispatch any events resulting from events that
         * might get triggered by the underlying streaming implementation.
         */
        public function get stopping() : Boolean {
            CONFIG::LOGGING {
            Log.info("stopping()");
            }
            return false;
        }

        /**
         * The playlist instance.
         */
        public function set playlist(playlist : Playlist) : void {
            // CONFIG::LOGGING {
            // Log.debug("set playlist()");
            // }
            _playlist = playlist;
            return;
        }

        public function get playlist() : Playlist {
            CONFIG::LOGGING {
            Log.debug("get playlist()");
            }
            return _playlist;
        }

        /**
         * Adds a callback public function to the NetConnection instance. This public function will fire ClipEvents whenever
         * the callback is invoked in the connection.
         * @param name
         * @param listener
         * @return
         * @see ClipEventType#CONNECTION_EVENT
         */
        public function addConnectionCallback(name : String, listener : Function) : void {
            CONFIG::LOGGING {
            Log.debug("addConnectionCallback()");
            }
            return;
        }

        /**
         * Adds a callback public function to the NetStream object. This public function will fire a ClipEvent of type StreamEvent whenever
         * the callback has been invoked on the stream. The invokations typically come from a server-side app running
         * on RTMP server.
         * @param name
         * @param listener
         * @return
         * @see ClipEventType.NETSTREAM_EVENT
         */
        public function addStreamCallback(name : String, listener : Function) : void {
            CONFIG::LOGGING {
            Log.debug("addStreamCallback()");
            }
            return;
        }

        /**
         * Get the current stream callbacks.
         * @return a dictionary of callbacks, keyed using callback names and values being the callback functions
         */
        public function get streamCallbacks() : Dictionary {
            CONFIG::LOGGING {
            Log.debug("get streamCallbacks()");
            }
            return null;
        }

        /**
         * Gets the underlying NetStream object.
         * @return the netStream currently in use, or null if this provider has not started streaming yet
         */
        public function get netStream() : NetStream {
            CONFIG::LOGGING {
            Log.debug("get netStream()");
            }
            return _hls.stream;
        }

        /**
         * Gets the underlying netConnection object.
         * @return the netConnection currently in use, or null if this provider has not started streaming yet
         */
        public function get netConnection() : NetConnection {
            CONFIG::LOGGING {
            Log.debug("get netConnection()");
            }
            return null;
        }

        /**
         * Sets a time provider to be used by this StreamProvider. Normally the playhead time is queried from
         * the NetStream.time property.
         *
         * @param timeProvider
         */
        public function set timeProvider(timeProvider : TimeProvider) : void {
            CONFIG::LOGGING {
            Log.debug("set timeProvider()");
            }
            _timeProvider = timeProvider;
            return;
        }

        /**
         * Gets the type of StreamProvider either http, rtmp, psuedo.
         */
        public function get type() : String {
            return "httpstreaming";
        }

        /**
         * Switch the stream in realtime with / without dynamic stream switching support
         *
         * @param event ClipEvent the clip event
         * @param clip Clip the clip to switch to
         * @param netStreamPlayOptions Object the NetStreamPlayOptions object to enable dynamic stream switching
         */
        public function switchStream(event : ClipEvent, clip : Clip, netStreamPlayOptions : Object = null) : void {
            CONFIG::LOGGING {
            Log.info("switchStream()");
            }
            return;
        }
    }
}

flashlsFlowPlayer.swf: fix seek bar not being updated when replaying a video


package aerys.minko.scene.controller.mesh.skinning
{
	import aerys.minko.Minko;
	import aerys.minko.ns.minko_math;
	import aerys.minko.render.Viewport;
	import aerys.minko.scene.controller.AbstractController;
	import aerys.minko.scene.controller.EnterFrameController;
	import aerys.minko.scene.controller.IRebindableController;
	import aerys.minko.scene.node.Group;
	import aerys.minko.scene.node.ISceneNode;
	import aerys.minko.scene.node.Mesh;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.animation.SkinningMethod;
	import aerys.minko.type.binding.DataBindings;
	import aerys.minko.type.log.DebugLevel;
	import aerys.minko.type.math.Matrix4x4;

	import flash.display.BitmapData;
	import flash.geom.Matrix3D;
	import flash.utils.Dictionary;

	public final class SkinningController extends EnterFrameController implements IRebindableController
	{
		public static const MAX_NUM_INFLUENCES	: uint	= 8;
		public static const MAX_NUM_JOINTS		: uint	= 51;

		private var _skinningHelper		: AbstractSkinningHelper;
		private var _isDirty			: Boolean;

		private var _method				: uint;
		private var _bindShapeMatrix	: Matrix3D;
		private var _skeletonRoot		: Group;
		private var _joints				: Vector.<Group>;
		private var _invBindMatrices	: Vector.<Matrix3D>;

		private var _numVisibleTargets	: uint;

		public function get skinningMethod() : uint
		{
			return _method;
		}

		public function get bindShape() : Matrix4x4
		{
			var bindShape : Matrix4x4 = new Matrix4x4();
			bindShape.minko_math::_matrix = _bindShapeMatrix;

			return bindShape;
		}

		public function get skeletonRoot() : Group
		{
			return _skeletonRoot;
		}

		public function get joints() : Vector.<Group>
		{
			return _joints;
		}

		public function get invBindMatrices() : Vector.<Matrix4x4>
		{
			var numMatrices		: uint					= _invBindMatrices.length;
			var invBindMatrices : Vector.<Matrix4x4>	= new Vector.<Matrix4x4>(numMatrices);

			for (var matrixId : uint = 0; matrixId < numMatrices; ++matrixId)
			{
				invBindMatrices[matrixId] = new Matrix4x4();
				invBindMatrices[matrixId].minko_math::_matrix = _invBindMatrices[matrixId];
			}

			return invBindMatrices;
		}

		public function SkinningController(method			: uint,
										   skeletonRoot		: Group,
										   joints			: Vector.<Group>,
										   bindShape		: Matrix4x4,
										   invBindMatrices	: Vector.<Matrix4x4>)
		{
			super(Mesh);

            if (!skeletonRoot)
                throw new Error('skeletonRoot cannot be null');

			_method				= method;
			_skeletonRoot		= skeletonRoot;
			_bindShapeMatrix	= bindShape.minko_math::_matrix;
			_isDirty			= false;

			initialize(joints, invBindMatrices);
		}

		private function initialize(joints			: Vector.<Group>,
									invBindMatrices	: Vector.<Matrix4x4>) : void
		{
			var numJoints : uint = joints.length;

			_joints	= joints.slice();

			_invBindMatrices	= new Vector.<Matrix3D>(numJoints, true);
			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				_invBindMatrices[jointId] = invBindMatrices[jointId].minko_math::_matrix;
		}

		override protected function targetAddedToScene(target : ISceneNode, scene : Scene) : void
		{
			super.targetAddedToScene(target, scene);

			if (getNumTargetsInScene(scene) == 1)
				subscribeToJoints();

			var mesh : Mesh = target as Mesh;

			mesh.bindings.addCallback('inFrustum', meshVisibilityChangedHandler);

			if (!_skinningHelper)
			{
				var numInfluences 	: uint = AbstractSkinningHelper.getNumInfluences(
                    mesh.geometry.format
                );

				if (_joints.length > MAX_NUM_JOINTS || numInfluences > MAX_NUM_INFLUENCES)
				{
					_method = SkinningMethod.SOFTWARE_MATRIX;
					Minko.log(
						DebugLevel.SKINNING,
						'Too many influences/joints: switching to SkinningMethod.SOFTWARE.'
					);
				}

				if (skinningMethod != SkinningMethod.SOFTWARE_MATRIX)
                {
                    try
                    {
    					_skinningHelper = new HardwareSkinningHelper(
    						_method, _bindShapeMatrix, _invBindMatrices
    					);
                    }
                    catch (e : Error)
                    {
                        Minko.log(
                            DebugLevel.SKINNING,
                            'Falling back to software skinning: ' + e.message,
                            this
                        );

                        _method = SkinningMethod.SOFTWARE_MATRIX;
                    }
                }

                _skinningHelper ||= new SoftwareSkinningHelper(
                    _method, _bindShapeMatrix, _invBindMatrices
                );
			}

			_skinningHelper.addMesh(mesh);

			_isDirty = true;
		}

		override protected function targetRemovedFromScene(target : ISceneNode, scene : Scene) : void
		{
			super.targetRemovedFromScene(target, scene);

			if (numTargets == 0)
				unsubscribeFromJoints();

			var mesh : Mesh = target as Mesh;

			mesh.bindings.removeCallback('inFrustum', meshVisibilityChangedHandler);
			_skinningHelper.removeMesh(mesh);
		}

		private function jointLocalToWorldChangedHandler(node : Group, emitter : Matrix4x4) : void
		{
			_isDirty = true;
		}

		public function rebindDependencies(nodeMap			: Dictionary,
										   controllerMap	: Dictionary) : void
		{
			var numJoints	: uint = _joints.length;

			if (numTargets != 0)
				unsubscribeFromJoints();

			if (_joints.indexOf(_skeletonRoot) == -1 && nodeMap[_skeletonRoot])
				_skeletonRoot = nodeMap[_skeletonRoot];

			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				if (nodeMap[_joints[jointId]])
					_joints[jointId] = nodeMap[_joints[jointId]];

			if (numTargets != 0)
				subscribeToJoints();

			_isDirty = true;
		}

		private function subscribeToJoints() : void
		{
			var numJoints	: uint = _joints.length;

			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				_joints[jointId].localToWorldTransformChanged.add(jointLocalToWorldChangedHandler);

			if (_joints.indexOf(_skeletonRoot) == -1)
				_skeletonRoot.localToWorldTransformChanged.add(jointLocalToWorldChangedHandler);
		}

		private function unsubscribeFromJoints() : void
		{
			var numJoints	: uint = _joints.length;

			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				_joints[jointId].localToWorldTransformChanged.remove(jointLocalToWorldChangedHandler);

			if (_joints.indexOf(_skeletonRoot) == -1)
				_skeletonRoot.localToWorldTransformChanged.remove(jointLocalToWorldChangedHandler);
		}

		override protected function sceneEnterFrameHandler(scene		: Scene,
														   viewport		: Viewport,
														   destination	: BitmapData,
														   time			: Number) : void
		{
			if (_isDirty && _skinningHelper.numMeshes != 0)
			{
				_skinningHelper.update(_skeletonRoot, _joints);
				_isDirty = false;
			}
		}

		override public function clone() : AbstractController
		{
			return new SkinningController(_method, _skeletonRoot, _joints, bindShape, invBindMatrices);
		}

		private function meshVisibilityChangedHandler(bindings 	: DataBindings,
												  property	: String,
												  oldValue	: Boolean,
												  newValue	: Boolean) : void
		{
			if (newValue)
				_skinningHelper.addMesh(bindings.owner as Mesh);
			else
				_skinningHelper.removeMesh(bindings.owner as Mesh);
		}
	}
}

package aerys.minko.scene.controller.mesh.skinning
{
	import aerys.minko.Minko;
	import aerys.minko.ns.minko_math;
	import aerys.minko.render.Viewport;
	import aerys.minko.scene.controller.AbstractController;
	import aerys.minko.scene.controller.EnterFrameController;
	import aerys.minko.scene.controller.IRebindableController;
	import aerys.minko.scene.node.Group;
	import aerys.minko.scene.node.ISceneNode;
	import aerys.minko.scene.node.Mesh;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.animation.SkinningMethod;
	import aerys.minko.type.binding.DataBindings;
	import aerys.minko.type.log.DebugLevel;
	import aerys.minko.type.math.Matrix4x4;

	import flash.display.BitmapData;
	import flash.geom.Matrix3D;
	import flash.utils.Dictionary;

	public final class SkinningController extends EnterFrameController implements IRebindableController
	{
		public static const MAX_NUM_INFLUENCES	: uint	= 8;
		public static const MAX_NUM_JOINTS		: uint	= 51;

		private var _skinningHelper		: AbstractSkinningHelper;
		private var _isDirty			: Boolean;

		private var _method				: uint;
		private var _bindShapeMatrix	: Matrix3D;
		private var _skeletonRoot		: Group;
		private var _joints				: Vector.<Group>;
		private var _invBindMatrices	: Vector.<Matrix3D>;

		private var _numVisibleTargets	: uint;

		public function get skinningMethod() : uint
		{
			return _method;
		}

		public function get bindShape() : Matrix4x4
		{
			var bindShape : Matrix4x4 = new Matrix4x4();
			bindShape.minko_math::_matrix = _bindShapeMatrix;

			return bindShape;
		}

		public function get skeletonRoot() : Group
		{
			return _skeletonRoot;
		}

		public function get joints() : Vector.<Group>
		{
			return _joints;
		}

		public function get invBindMatrices() : Vector.<Matrix4x4>
		{
			var numMatrices		: uint					= _invBindMatrices.length;
			var invBindMatrices : Vector.<Matrix4x4>	= new Vector.<Matrix4x4>(numMatrices);

			for (var matrixId : uint = 0; matrixId < numMatrices; ++matrixId)
			{
				invBindMatrices[matrixId] = new Matrix4x4();
				invBindMatrices[matrixId].minko_math::_matrix = _invBindMatrices[matrixId];
			}

			return invBindMatrices;
		}

		public function SkinningController(method			: uint,
										   skeletonRoot		: Group,
										   joints			: Vector.<Group>,
										   bindShape		: Matrix4x4,
										   invBindMatrices	: Vector.<Matrix4x4>)
		{
			super(Mesh);

            if (!skeletonRoot)
                throw new Error('skeletonRoot cannot be null');

			_method				= method;
			_skeletonRoot		= skeletonRoot;
			_bindShapeMatrix	= bindShape.minko_math::_matrix;
			_isDirty			= false;

			initialize(joints, invBindMatrices);
		}

		private function initialize(joints			: Vector.<Group>,
									invBindMatrices	: Vector.<Matrix4x4>) : void
		{
			var numJoints : uint = joints.length;

			_joints	= joints.slice();

			_invBindMatrices	= new Vector.<Matrix3D>(numJoints, true);
			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				_invBindMatrices[jointId] = invBindMatrices[jointId].minko_math::_matrix;
		}

		override protected function targetAddedToScene(target : ISceneNode, scene : Scene) : void
		{
			super.targetAddedToScene(target, scene);

			if (getNumTargetsInScene(scene) == 1)
				subscribeToJoints();

			var mesh : Mesh = target as Mesh;

			mesh.bindings.addCallback('insideFrustum', meshVisibilityChangedHandler);

			if (!_skinningHelper)
			{
				var numInfluences 	: uint = AbstractSkinningHelper.getNumInfluences(
                    mesh.geometry.format
                );

				if (_joints.length > MAX_NUM_JOINTS || numInfluences > MAX_NUM_INFLUENCES)
				{
					_method = SkinningMethod.SOFTWARE_MATRIX;
					Minko.log(
						DebugLevel.SKINNING,
						'Too many influences/joints: switching to SkinningMethod.SOFTWARE.'
					);
				}

				if (skinningMethod != SkinningMethod.SOFTWARE_MATRIX)
                {
                    try
                    {
    					_skinningHelper = new HardwareSkinningHelper(
    						_method, _bindShapeMatrix, _invBindMatrices
    					);
                    }
                    catch (e : Error)
                    {
                        Minko.log(
                            DebugLevel.SKINNING,
                            'Falling back to software skinning: ' + e.message,
                            this
                        );

                        _method = SkinningMethod.SOFTWARE_MATRIX;
                    }
                }

                _skinningHelper ||= new SoftwareSkinningHelper(
                    _method, _bindShapeMatrix, _invBindMatrices
                );
			}

			_skinningHelper.addMesh(mesh);

			_isDirty = true;
		}

		override protected function targetRemovedFromScene(target : ISceneNode, scene : Scene) : void
		{
			super.targetRemovedFromScene(target, scene);

			if (getNumTargetsInScene(scene) == 0)
				unsubscribeFromJoints();

			var mesh : Mesh = target as Mesh;

			mesh.bindings.removeCallback('insideFrustum', meshVisibilityChangedHandler);
			_skinningHelper.removeMesh(mesh);
		}

		private function jointLocalToWorldChangedHandler(node : Group, emitter : Matrix4x4) : void
		{
			_isDirty = true;
		}

		public function rebindDependencies(nodeMap			: Dictionary,
										   controllerMap	: Dictionary) : void
		{
			var numJoints	: uint = _joints.length;

			if (numTargets != 0)
				unsubscribeFromJoints();

			if (_joints.indexOf(_skeletonRoot) == -1 && nodeMap[_skeletonRoot])
				_skeletonRoot = nodeMap[_skeletonRoot];

			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				if (nodeMap[_joints[jointId]])
					_joints[jointId] = nodeMap[_joints[jointId]];

			if (numTargets != 0)
				subscribeToJoints();

			_isDirty = true;
		}

		private function subscribeToJoints() : void
		{
			var numJoints	: uint = _joints.length;

			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				_joints[jointId].localToWorldTransformChanged.add(jointLocalToWorldChangedHandler);

			if (_joints.indexOf(_skeletonRoot) == -1)
				_skeletonRoot.localToWorldTransformChanged.add(jointLocalToWorldChangedHandler);
		}

		private function unsubscribeFromJoints() : void
		{
			var numJoints	: uint = _joints.length;

			for (var jointId : uint = 0; jointId < numJoints; ++jointId)
				_joints[jointId].localToWorldTransformChanged.remove(jointLocalToWorldChangedHandler);

			if (_joints.indexOf(_skeletonRoot) == -1)
				_skeletonRoot.localToWorldTransformChanged.remove(jointLocalToWorldChangedHandler);
		}

		override protected function sceneEnterFrameHandler(scene		: Scene,
														   viewport		: Viewport,
														   destination	: BitmapData,
														   time			: Number) : void
		{
			if (_isDirty && _skinningHelper.numMeshes != 0)
			{
				_skinningHelper.update(_skeletonRoot, _joints);
				_isDirty = false;
			}
		}

		override public function clone() : AbstractController
		{
			return new SkinningController(_method, _skeletonRoot, _joints, bindShape, invBindMatrices);
		}

		private function meshVisibilityChangedHandler(bindings 	: DataBindings,
												  property	: String,
												  oldValue	: Boolean,
												  newValue	: Boolean) : void
		{
			if (newValue)
				_skinningHelper.addMesh(bindings.owner as Mesh);
			else
				_skinningHelper.removeMesh(bindings.owner as Mesh);
		}
	}
}

fix broken reference to the 'inFrustum' mesh binding


/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mangui.hls {
    import org.mangui.hls.constant.HLSSeekMode;
    import org.mangui.hls.constant.HLSMaxLevelCappingMode;

    public final class HLSSettings extends Object {
        /**
         * Limit levels usable in auto-quality by the stage dimensions (width and height).
         *      true - level width and height (defined in m3u8 playlist) will be compared with the player width and height (stage.stageWidth and stage.stageHeight).
         *             Max level will be set depending on the maxLevelCappingMode option.
         *      false - levels will not be limited. All available levels could be used in auto-quality mode taking only bandwidth into consideration.
         *
         * Note: this setting is ignored in manual mode so all the levels could be selected manually.
         *
         * Default is false
         */
        public static var capLevelToStage : Boolean = false;
        /**
         * Defines the max level capping mode to the one available in HLSMaxLevelCappingMode
         *      HLSMaxLevelCappingMode.DOWNSCALE - max capped level should be the one with the dimensions equal or greater than the stage dimensions (so the video will be downscaled)
         *      HLSMaxLevelCappingMode.UPSCALE - max capped level should be the one with the dimensions equal or lower than the stage dimensions (so the video will be upscaled)
         *
         * Default is HLSMaxLevelCappingMode.DOWNSCALE
         */
        public static var maxLevelCappingMode : String = HLSMaxLevelCappingMode.DOWNSCALE;
        // // // // // // /////////////////////////////////
        //
        // org.mangui.hls.stream.HLSNetStream
        //
        // // // // // // /////////////////////////////////
        /**
         * Defines minimum buffer length in seconds before playback can start, after seeking or buffer stalling.
         *
         * Default is -1 = auto
         */
        public static var minBufferLength : Number = -1;
        /**
         * Defines maximum buffer length in seconds.
         * (0 means infinite buffering)
         *
         * Default is 120.
         */
        public static var maxBufferLength : Number = 120;
        /**
         * Defines maximum back buffer length in seconds.
         * (0 means infinite back buffering)
         *
         * Default is 30.
         */
        public static var maxBackBufferLength : Number = 30;
        /**
         * Defines low buffer length in seconds.
         * When crossing down this threshold, HLS will switch to buffering state.
         *
         * Default is 3.
         */
        public static var lowBufferLength : Number = 3;
        /**
         * Defines seek mode to one form available in HLSSeekMode class:
         *      HLSSeekMode.ACCURATE_SEEK - accurate seeking to exact requested position
         *      HLSSeekMode.KEYFRAME_SEEK - key-frame based seeking (seek to nearest key frame before requested seek position)
         *
         * Default is HLSSeekMode.KEYFRAME_SEEK.
         */
        public static var seekMode : String = HLSSeekMode.KEYFRAME_SEEK;
        /** max nb of retries for Key Loading in case I/O errors are met,
         *      0, means no retry, error will be triggered automatically
         *     -1 means infinite retry
         */
        public static var keyLoadMaxRetry : int = -1;
        /** keyLoadMaxRetryTimeout

         * Maximum key retry timeout (in milliseconds) in case I/O errors are met.
         * Every fail on key request, player will exponentially increase the timeout to try again.
         * It starts waiting 1 second (1000ms), than 2, 4, 8, 16, until keyLoadMaxRetryTimeout is reached.
         *
         * Default is 64000.
         */
        public static var keyLoadMaxRetryTimeout : Number = 64000;
        /** max nb of retries for Fragment Loading in case I/O errors are met,
         *      0, means no retry, error will be triggered automatically
         *     -1 means infinite retry
         */
        public static var fragmentLoadMaxRetry : int = 4000;
        /** fragmentLoadMaxRetryTimeout

         * Maximum Fragment retry timeout (in milliseconds) in case I/O errors are met.
         * Every fail on fragment request, player will exponentially increase the timeout to try again.
         * It starts waiting 1 second (1000ms), than 2, 4, 8, 16, until fragmentLoadMaxRetryTimeout is reached.
         *
         * Default is 4000.
         */
        public static var fragmentLoadMaxRetryTimeout : Number = 64000;
        /** fragmentLoadSkipAfterMaxRetry

         * control behaviour in case fragment load still fails after max retry timeout
         * if set to true, fragment will be skipped and next one will be loaded.
         * If set to false, an I/O Error will be raised.
         *
         * Default is true.
         */
        public static var fragmentLoadSkipAfterMaxRetry : Boolean = true;
        /**
         * If set to true, live playlist will be flushed from URL cache before reloading
         * (this is to workaround some cache issues with some combination of Flash Player / IE version)
         *
         * Default is false
         */
        public static var flushLiveURLCache : Boolean = false;
        /** max nb of retries for Manifest Loading in case I/O errors are met,
         *      0, means no retry, error will be triggered automatically
         *     -1 means infinite retry
         */
        public static var manifestLoadMaxRetry : int = -1;
        /** manifestLoadMaxRetryTimeout

         * Maximum Manifest retry timeout (in milliseconds) in case I/O errors are met.
         * Every fail on fragment request, player will exponentially increase the timeout to try again.
         * It starts waiting 1 second (1000ms), than 2, 4, 8, 16, until manifestLoadMaxRetryTimeout is reached.
         *
         * Default is 64000.
         */
        public static var manifestLoadMaxRetryTimeout : Number = 64000;
        /**
         * If greater than 0, specifies the preferred bitrate.
         * If -1, and startFromLevel is not specified, automatic start level selection will be used.
         * This parameter, if set, will take priority over startFromLevel.
         */
        public static var startFromBitrate : Number = -1;
        /** start level :
         *  from 0 to 1 : indicates the "normalized" preferred level. As such, if it is 0.5, the closest to the middle bitrate will be selected and used first.
         * -1 : automatic start level selection, playback will start from level matching download bandwidth (determined from download of first segment)
         * -2 : playback will start from the first level appearing in Manifest (not sorted by bitrate)
         */
        public static var startFromLevel : Number = -1;
        /** seek level :
         *  from 0 to 1 : indicates the "normalized" preferred bitrate. As such, if it is 0.5, the closest to the middle bitrate will be selected and used first.
         * -1 : automatic start level selection, keep previous level matching previous download bandwidth
         */
        public static var seekFromLevel : Number = -1;
        /** use hardware video decoder :
         *  it will set NetStream.useHardwareDecoder
         *  refer to http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/NetStream.html#useHardwareDecoder
         */
        public static var useHardwareVideoDecoder : Boolean = false;
        /**
         * Defines whether INFO level log messages will will appear in the console
         * Default is true.
         */
        public static var logInfo : Boolean = true;
        /**
         * Defines whether DEBUG level log messages will will appear in the console
         * Default is false.
         */
        public static var logDebug : Boolean = false;
        /**
         * Defines whether DEBUG2 level log messages will will appear in the console
         * Default is false.
         */
        public static var logDebug2 : Boolean = false;
        /**
         * Defines whether WARN level log messages will will appear in the console
         * Default is true.
         */
        public static var logWarn : Boolean = true;
        /**
         * Defines whether ERROR level log messages will will appear in the console
         * Default is true.
         */
        public static var logError : Boolean = true;
    }
}

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mangui.hls {
    import org.mangui.hls.constant.HLSSeekMode;
    import org.mangui.hls.constant.HLSMaxLevelCappingMode;

    public final class HLSSettings extends Object {
        /**
         * Limit levels usable in auto-quality by the stage dimensions (width and height).
         *      true - level width and height (defined in m3u8 playlist) will be compared with the player width and height (stage.stageWidth and stage.stageHeight).
         *             Max level will be set depending on the maxLevelCappingMode option.
         *      false - levels will not be limited. All available levels could be used in auto-quality mode taking only bandwidth into consideration.
         *
         * Note: this setting is ignored in manual mode so all the levels could be selected manually.
         *
         * Default is false
         */
        public static var capLevelToStage : Boolean = false;
        /**
         * Defines the max level capping mode to the one available in HLSMaxLevelCappingMode
         *      HLSMaxLevelCappingMode.DOWNSCALE - max capped level should be the one with the dimensions equal or greater than the stage dimensions (so the video will be downscaled)
         *      HLSMaxLevelCappingMode.UPSCALE - max capped level should be the one with the dimensions equal or lower than the stage dimensions (so the video will be upscaled)
         *
         * Default is HLSMaxLevelCappingMode.DOWNSCALE
         */
        public static var maxLevelCappingMode : String = HLSMaxLevelCappingMode.DOWNSCALE;
        // // // // // // /////////////////////////////////
        //
        // org.mangui.hls.stream.HLSNetStream
        //
        // // // // // // /////////////////////////////////
        /**
         * Defines minimum buffer length in seconds before playback can start, after seeking or buffer stalling.
         *
         * Default is -1 = auto
         */
        public static var minBufferLength : Number = -1;
        /**
         * Defines maximum buffer length in seconds.
         * (0 means infinite buffering)
         *
         * Default is 120.
         */
        public static var maxBufferLength : Number = 120;
        /**
         * Defines maximum back buffer length in seconds.
         * (0 means infinite back buffering)
         *
         * Default is 30.
         */
        public static var maxBackBufferLength : Number = 30;
        /**
         * Defines low buffer length in seconds.
         * When crossing down this threshold, HLS will switch to buffering state.
         *
         * Default is 3.
         */
        public static var lowBufferLength : Number = 3;
        /**
         * Defines seek mode to one form available in HLSSeekMode class:
         *      HLSSeekMode.ACCURATE_SEEK - accurate seeking to exact requested position
         *      HLSSeekMode.KEYFRAME_SEEK - key-frame based seeking (seek to nearest key frame before requested seek position)
         *
         * Default is HLSSeekMode.KEYFRAME_SEEK.
         */
        public static var seekMode : String = HLSSeekMode.KEYFRAME_SEEK;
        /** max nb of retries for Key Loading in case I/O errors are met,
         *      0, means no retry, error will be triggered automatically
         *     -1 means infinite retry
         */
        public static var keyLoadMaxRetry : int = 3;
        /** keyLoadMaxRetryTimeout

         * Maximum key retry timeout (in milliseconds) in case I/O errors are met.
         * Every fail on key request, player will exponentially increase the timeout to try again.
         * It starts waiting 1 second (1000ms), than 2, 4, 8, 16, until keyLoadMaxRetryTimeout is reached.
         *
         * Default is 64000.
         */
        public static var keyLoadMaxRetryTimeout : Number = 64000;
        /** max nb of retries for Fragment Loading in case I/O errors are met,
         *      0, means no retry, error will be triggered automatically
         *     -1 means infinite retry
         */
        public static var fragmentLoadMaxRetry : int = 3;
        /** fragmentLoadMaxRetryTimeout

         * Maximum Fragment retry timeout (in milliseconds) in case I/O errors are met.
         * Every fail on fragment request, player will exponentially increase the timeout to try again.
         * It starts waiting 1 second (1000ms), than 2, 4, 8, 16, until fragmentLoadMaxRetryTimeout is reached.
         *
         * Default is 4000.
         */
        public static var fragmentLoadMaxRetryTimeout : Number = 64000;
        /** fragmentLoadSkipAfterMaxRetry

         * control behaviour in case fragment load still fails after max retry timeout
         * if set to true, fragment will be skipped and next one will be loaded.
         * If set to false, an I/O Error will be raised.
         *
         * Default is true.
         */
        public static var fragmentLoadSkipAfterMaxRetry : Boolean = true;
        /**
         * If set to true, live playlist will be flushed from URL cache before reloading
         * (this is to workaround some cache issues with some combination of Flash Player / IE version)
         *
         * Default is false
         */
        public static var flushLiveURLCache : Boolean = false;
        /** max nb of retries for Manifest Loading in case I/O errors are met,
         *      0, means no retry, error will be triggered automatically
         *     -1 means infinite retry
         */
        public static var manifestLoadMaxRetry : int = -1;
        /** manifestLoadMaxRetryTimeout

         * Maximum Manifest retry timeout (in milliseconds) in case I/O errors are met.
         * Every fail on fragment request, player will exponentially increase the timeout to try again.
         * It starts waiting 1 second (1000ms), than 2, 4, 8, 16, until manifestLoadMaxRetryTimeout is reached.
         *
         * Default is 64000.
         */
        public static var manifestLoadMaxRetryTimeout : Number = 64000;
        /**
         * If greater than 0, specifies the preferred bitrate.
         * If -1, and startFromLevel is not specified, automatic start level selection will be used.
         * This parameter, if set, will take priority over startFromLevel.
         */
        public static var startFromBitrate : Number = -1;
        /** start level :
         *  from 0 to 1 : indicates the "normalized" preferred level. As such, if it is 0.5, the closest to the middle bitrate will be selected and used first.
         * -1 : automatic start level selection, playback will start from level matching download bandwidth (determined from download of first segment)
         * -2 : playback will start from the first level appearing in Manifest (not sorted by bitrate)
         */
        public static var startFromLevel : Number = -1;
        /** seek level :
         *  from 0 to 1 : indicates the "normalized" preferred bitrate. As such, if it is 0.5, the closest to the middle bitrate will be selected and used first.
         * -1 : automatic start level selection, keep previous level matching previous download bandwidth
         */
        public static var seekFromLevel : Number = -1;
        /** use hardware video decoder :
         *  it will set NetStream.useHardwareDecoder
         *  refer to http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/NetStream.html#useHardwareDecoder
         */
        public static var useHardwareVideoDecoder : Boolean = false;
        /**
         * Defines whether INFO level log messages will will appear in the console
         * Default is true.
         */
        public static var logInfo : Boolean = true;
        /**
         * Defines whether DEBUG level log messages will will appear in the console
         * Default is false.
         */
        public static var logDebug : Boolean = false;
        /**
         * Defines whether DEBUG2 level log messages will will appear in the console
         * Default is false.
         */
        public static var logDebug2 : Boolean = false;
        /**
         * Defines whether WARN level log messages will will appear in the console
         * Default is true.
         */
        public static var logWarn : Boolean = true;
        /**
         * Defines whether ERROR level log messages will will appear in the console
         * Default is true.
         */
        public static var logError : Boolean = true;
    }
}

set fragment/key max retry to 3 related to #262