#include "pch.h"
#include "log.h"
#include "canvas.h"
#include "app.h"
#include "texture.h"
#include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
#include <array>
#include <thread>
#include <algorithm>
#include <cstdint>
#include <numeric>

#ifdef __APPLE__
#include <Foundation/Foundation.h>
#endif

namespace {

GLint current_canvas_stroke_internal_format()
{
    const auto renderer_features = ShaderManager::render_device_features();
    if (renderer_features.float32_linear_filtering)
        return static_cast<GLint>(pp::renderer::gl::rgba32f_internal_format());
    if (renderer_features.float16_render_targets)
        return static_cast<GLint>(pp::renderer::gl::rgba16f_internal_format());
    return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}

GLint rgba8_internal_format()
{
    return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}

pp::renderer::RenderDeviceFeatures canvas_render_device_features() noexcept
{
    return ShaderManager::render_device_features();
}

pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_destination_feedback_plan(
    int width,
    int height) noexcept
{
    const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback(
        canvas_render_device_features(),
        pp::renderer::Extent2D {
            .width = static_cast<std::uint32_t>(std::max(width, 0)),
            .height = static_cast<std::uint32_t>(std::max(height, 0)),
        });
    if (plan) {
        return plan.value();
    }

    pp::paint_renderer::CanvasStrokeFeedbackPlan fallback;
    fallback.compatibility_fallback = true;
    fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures;
    fallback.requires_auxiliary_texture = true;
    return fallback;
}

pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
    int width,
    int height,
    const std::vector<std::shared_ptr<Layer>>& layers,
    const Brush* brush) noexcept
{
    std::vector<int> layer_blend_modes;
    layer_blend_modes.reserve(layers.size());
    for (const auto& layer : layers) {
        if (!layer) {
            continue;
        }
        layer_blend_modes.push_back(layer->m_blend_mode);
    }

    const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
        canvas_render_device_features(),
        pp::paint_renderer::CanvasBlendGateRequest {
            .extent = pp::renderer::Extent2D {
                .width = static_cast<std::uint32_t>(std::max(width, 0)),
                .height = static_cast<std::uint32_t>(std::max(height, 0)),
            },
            .layer_blend_modes = layer_blend_modes,
            .has_stroke_blend_mode = brush != nullptr,
            .stroke_blend_mode = brush ? brush->m_blend_mode : 0,
        });
    if (plan) {
        return plan.value();
    }

    pp::paint_renderer::CanvasBlendGatePlan fallback;
    fallback.shader_blend = true;
    fallback.complex_blend = true;
    fallback.compatibility_fallback = true;
    return fallback;
}

GLenum depth_test_state()
{
    return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}

GLenum scissor_test_state()
{
    return static_cast<GLenum>(pp::renderer::gl::scissor_test_state());
}

GLenum blend_state()
{
    return static_cast<GLenum>(pp::renderer::gl::blend_state());
}

GLint texture_filter_linear()
{
    return static_cast<GLint>(pp::renderer::gl::linear_texture_filter());
}

GLint texture_filter_linear_mipmap_linear()
{
    return static_cast<GLint>(pp::renderer::gl::linear_mipmap_linear_texture_filter());
}

GLint texture_filter_nearest()
{
    return static_cast<GLint>(pp::renderer::gl::nearest_texture_filter());
}

GLint texture_wrap_repeat()
{
    return static_cast<GLint>(pp::renderer::gl::repeat_texture_wrap());
}

GLint texture_wrap_clamp_to_border()
{
    return static_cast<GLint>(pp::renderer::gl::clamp_to_border_texture_wrap());
}

pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count)
{
    return pp::renderer::gl::texture_format_for_channel_count(static_cast<std::uint32_t>(channel_count));
}

void activate_opengl_texture(std::uint32_t texture_unit) noexcept
{
    glActiveTexture(static_cast<GLenum>(texture_unit));
}

void bind_opengl_texture(std::uint32_t target, std::uint32_t texture) noexcept
{
    glBindTexture(static_cast<GLenum>(target), static_cast<GLuint>(texture));
}

void set_active_texture_unit(std::uint32_t unit_index)
{
    const auto status = pp::renderer::gl::activate_opengl_texture_unit(
        unit_index,
        pp::renderer::gl::OpenGlActiveTextureDispatch {
            .active_texture = activate_opengl_texture,
        });
    if (!status.ok())
        LOG("Canvas active texture dispatch failed because: %s", status.message);
}

void unbind_texture_2d()
{
    const auto status = pp::renderer::gl::bind_opengl_texture_2d(
        0U,
        pp::renderer::gl::OpenGlTexture2DBindDispatch {
            .bind_texture = bind_opengl_texture,
        });
    if (!status.ok())
        LOG("Canvas texture unbind dispatch failed because: %s", status.message);
}

void enable_opengl_state(std::uint32_t state) noexcept
{
    glEnable(static_cast<GLenum>(state));
}

void disable_opengl_state(std::uint32_t state) noexcept
{
    glDisable(static_cast<GLenum>(state));
}

std::uint8_t query_opengl_capability(std::uint32_t state) noexcept
{
    return static_cast<std::uint8_t>(glIsEnabled(static_cast<GLenum>(state)));
}

void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
    glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}

void set_opengl_clear_color(float r, float g, float b, float a) noexcept
{
    glClearColor(r, g, b, a);
}

void get_opengl_integer(std::uint32_t name, std::int32_t* values) noexcept
{
    GLint raw_values[4] {};
    glGetIntegerv(static_cast<GLenum>(name), raw_values);
    values[0] = static_cast<std::int32_t>(raw_values[0]);
    values[1] = static_cast<std::int32_t>(raw_values[1]);
    values[2] = static_cast<std::int32_t>(raw_values[2]);
    values[3] = static_cast<std::int32_t>(raw_values[3]);
}

void get_opengl_float(std::uint32_t name, float* values) noexcept
{
    glGetFloatv(static_cast<GLenum>(name), values);
}

void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
    glScissor(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}

void apply_canvas_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    const auto status = pp::renderer::gl::apply_opengl_viewport(
        pp::renderer::gl::OpenGlViewportRect {
            .x = x,
            .y = y,
            .width = width,
            .height = height,
        },
        pp::renderer::gl::OpenGlViewportDispatch {
            .viewport = set_opengl_viewport,
        });
    if (!status.ok())
        LOG("Canvas viewport dispatch failed because: %s", status.message);
}

pp::renderer::gl::OpenGlViewportRect query_canvas_viewport()
{
    const auto result = pp::renderer::gl::query_opengl_viewport(
        pp::renderer::gl::OpenGlViewportQueryDispatch {
            .get_integer = get_opengl_integer,
        });
    if (!result.ok()) {
        LOG("Canvas viewport query dispatch failed because: %s", result.status().message);
    }
    return result.value();
}

std::array<float, 4> query_canvas_clear_color()
{
    const auto result = pp::renderer::gl::query_opengl_clear_color(
        pp::renderer::gl::OpenGlClearColorQueryDispatch {
            .get_float = get_opengl_float,
        });
    if (!result.ok()) {
        LOG("Canvas clear-color query dispatch failed because: %s", result.status().message);
    }
    return result.value();
}

void apply_canvas_clear_color(std::array<float, 4> color)
{
    const auto status = pp::renderer::gl::apply_opengl_clear_color(
        color,
        pp::renderer::gl::OpenGlClearColorDispatch {
            .clear_color = set_opengl_clear_color,
        });
    if (!status.ok())
        LOG("Canvas clear-color dispatch failed because: %s", status.message);
}

void apply_canvas_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    const auto status = pp::renderer::gl::apply_opengl_scissor_rect(
        pp::renderer::gl::OpenGlScissorRect {
            .enabled = 1U,
            .x = x,
            .y = y,
            .width = width,
            .height = height,
        },
        pp::renderer::gl::OpenGlScissorDispatch {
            .enable = enable_opengl_state,
            .disable = disable_opengl_state,
            .scissor = set_opengl_scissor,
        });
    if (!status.ok())
        LOG("Canvas scissor dispatch failed because: %s", status.message);
}

void apply_canvas_capability(std::uint32_t state, bool enabled)
{
    const auto status = pp::renderer::gl::apply_opengl_capability(
        state,
        enabled,
        pp::renderer::gl::OpenGlCapabilityDispatch {
            .enable = enable_opengl_state,
            .disable = disable_opengl_state,
        });
    if (!status.ok())
        LOG("Canvas capability dispatch failed because: %s", status.message);
}

bool query_canvas_capability(std::uint32_t state)
{
    const auto result = pp::renderer::gl::query_opengl_capability_state(
        state,
        pp::renderer::gl::OpenGlCapabilityStateQueryDispatch {
            .is_enabled = query_opengl_capability,
        });
    if (!result.ok()) {
        LOG("Canvas capability query dispatch failed because: %s", result.status().message);
    }
    return result.value();
}

void gen_opengl_renderbuffers(std::uint32_t count, std::uint32_t* ids) noexcept
{
    glGenRenderbuffers(static_cast<GLsizei>(count), reinterpret_cast<GLuint*>(ids));
}

void delete_opengl_renderbuffers(std::uint32_t count, const std::uint32_t* ids) noexcept
{
    glDeleteRenderbuffers(static_cast<GLsizei>(count), reinterpret_cast<const GLuint*>(ids));
}

void bind_opengl_renderbuffer(std::uint32_t target, std::uint32_t renderbuffer) noexcept
{
    glBindRenderbuffer(static_cast<GLenum>(target), static_cast<GLuint>(renderbuffer));
}

void set_opengl_renderbuffer_storage(
    std::uint32_t target,
    std::uint32_t internal_format,
    std::int32_t width,
    std::int32_t height) noexcept
{
    glRenderbufferStorage(
        static_cast<GLenum>(target),
        static_cast<GLenum>(internal_format),
        static_cast<GLsizei>(width),
        static_cast<GLsizei>(height));
}

void attach_opengl_framebuffer_renderbuffer(
    std::uint32_t target,
    std::uint32_t attachment,
    std::uint32_t renderbuffer_target,
    std::uint32_t renderbuffer) noexcept
{
    glFramebufferRenderbuffer(
        static_cast<GLenum>(target),
        static_cast<GLenum>(attachment),
        static_cast<GLenum>(renderbuffer_target),
        static_cast<GLuint>(renderbuffer));
}

GLuint allocate_canvas_depth_renderbuffer(int width, int height)
{
    const auto result = pp::renderer::gl::allocate_opengl_depth_renderbuffer(
        width,
        height,
        pp::renderer::gl::OpenGlDepthRenderbufferAllocationDispatch {
            .gen_renderbuffers = gen_opengl_renderbuffers,
            .bind_renderbuffer = bind_opengl_renderbuffer,
            .renderbuffer_storage = set_opengl_renderbuffer_storage,
        });
    if (!result.ok()) {
        LOG("OpenGL canvas depth renderbuffer allocation failed: %s", result.status().message);
        return 0U;
    }
    return static_cast<GLuint>(result.value());
}

void attach_canvas_depth_renderbuffer(GLuint renderbuffer)
{
    const auto status = pp::renderer::gl::attach_opengl_depth_renderbuffer(
        static_cast<std::uint32_t>(renderbuffer),
        pp::renderer::gl::OpenGlDepthRenderbufferAttachmentDispatch {
            .framebuffer_renderbuffer = attach_opengl_framebuffer_renderbuffer,
        });
    if (!status.ok())
        LOG("OpenGL canvas depth renderbuffer attachment failed: %s", status.message);
}

void delete_canvas_renderbuffer(GLuint renderbuffer)
{
    const auto status = pp::renderer::gl::delete_opengl_renderbuffer(
        static_cast<std::uint32_t>(renderbuffer),
        pp::renderer::gl::OpenGlRenderbufferDeleteDispatch {
            .delete_renderbuffers = delete_opengl_renderbuffers,
        });
    if (!status.ok())
        LOG("OpenGL canvas renderbuffer delete failed: %s", status.message);
}

}


Canvas* Canvas::I;
std::vector<CanvasMode*> Canvas::modes[] = {
    { new CanvasModePen, new CanvasModeBasicCamera },       // brush
    { new CanvasModePen, new CanvasModeBasicCamera },       // eraser
    { new CanvasModeLine, new CanvasModeBasicCamera },      // line
    { new CanvasModeCamera, new CanvasModeBasicCamera },    // parallax
    { new CanvasModeGrid, new CanvasModeBasicCamera },      // grids
    { new CanvasModeTransform, new CanvasModeBasicCamera }, // import
    { new CanvasModeTransform, new CanvasModeBasicCamera }, // cut
    { new CanvasModeTransform, new CanvasModeBasicCamera }, // copy
    { new CanvasModeFill, new CanvasModeBasicCamera },      // fill
    { new CanvasModeMaskFree, new CanvasModeBasicCamera },  // mask-free
    { new CanvasModeMaskLine, new CanvasModeBasicCamera },  // mask-poly
    { new CanvasModeFloodFill, new CanvasModeBasicCamera },  // flood-fill
};
glm::vec3 Canvas::m_plane_origin[6] = {
    { 0, 0,-1}, // front
    { 1, 0, 0}, // right
    { 0, 0, 1}, // back
    {-1, 0, 0}, // left
    { 0, 1, 0}, // top
    { 0,-1, 0}, // bottom
};
glm::vec3 Canvas::m_plane_normal[6] = {
    { 0, 0, 1}, // front
    {-1, 0, 0}, // right
    { 0, 0,-1}, // back
    { 1, 0, 0}, // left
    { 0,-1, 0}, // top
    { 0, 1, 0}, // bottom
};
glm::vec3 Canvas::m_plane_tangent[6] = {
    {0, 1, 0}, // front
    {0, 1, 0}, // right
    {0, 1, 0}, // back
    {0, 1, 0}, // left
    {0, 0,-1}, // top
    {0, 0, 1}, // bottom
};
// only rotation
glm::mat4 Canvas::m_plane_transform[6] = {
    glm::lookAt(glm::vec3(), { 0, 0,-1}, {0, 1, 0}), // front
    glm::lookAt(glm::vec3(), {-1, 0, 0}, {0, 1, 0}), // right
    glm::lookAt(glm::vec3(), { 0, 0, 1}, {0, 1, 0}), // back
    glm::lookAt(glm::vec3(), { 1, 0, 0}, {0, 1, 0}), // left
    glm::lookAt(glm::vec3(), { 0, 1, 0}, {0, 0,-1}), // top
    glm::lookAt(glm::vec3(), { 0,-1, 0}, {0, 0, 1}), // bottom
};

void Canvas::pick_start()
{
    for (int i = 0; i < 6; i++)
        m_pick_ready[i] = false;
}
void Canvas::pick_update(int plane)
{
    // check if already updated
    if (m_pick_ready[plane])
        return;
    
    App::I->render_task([this, plane]
    {
        std::array<bool, 6> faces{ false };
        faces[plane] = true;
        draw_merge(true, faces);

        int i = plane;
        if (!m_pick_data[plane])
            m_pick_data[plane] = std::make_unique<glm::u8vec4[]>(m_width * m_height);
        m_layers_merge.rtt(i).readPixelsRgba8(
            0,
            0,
            m_width,
            m_height,
            m_pick_data[plane].get());
    });
    
    m_pick_ready[plane] = true;
}
glm::vec4 Canvas::pick_get(glm::vec2 canvas_loc)
{
    glm::vec3 ray_origin;
    glm::vec3 ray_dir;
    glm::vec3 hit_pos;
    glm::vec3 hit_normal;
    glm::vec2 fb_pos;
    int plane_id;
    if (point_trace(canvas_loc, ray_origin, ray_dir, hit_pos, fb_pos, hit_normal, plane_id))
    {
        pick_update(plane_id);
        int i = (int)fb_pos.x + (int)fb_pos.y * m_width;
        return glm::vec4(m_pick_data[plane_id][i]) / 255.f;
    }
    return {0,0,0,1};
}
void Canvas::pick_end()
{
    for (int i = 0; i < 6; i++)
        m_pick_data[i].release();
}
void Canvas::clear(const glm::vec4& c/*={0,0,0,1}*/)
{
    auto a = new ActionLayerClear;
    a->m_layer = m_layers[m_current_layer_idx];
    a->m_frame = layer().m_frame_index;
    a->m_snap = std::make_shared<LayerFrame::Snapshot>(a->m_layer->snapshot());
    a->m_color = c;
    ActionManager::add(a);

    m_layers[m_current_layer_idx]->clear(c);
    m_unsaved = true;
}
void Canvas::clear_all()
{
    for (auto& l : m_layers)
        l->clear({0, 0, 0, 0});
}
void Canvas::stroke_end()
{
    if (!m_current_stroke)
        return;
    if (m_current_stroke->has_sample())
    {
        m_commit_delayed = true;
    }
    else
    {
        m_show_tmp = false;
        stroke_commit();
        m_current_stroke = nullptr;
    }
}
void Canvas::stroke_cancel()
{
    if (!m_current_stroke)
        return;
    m_current_stroke = nullptr;
    m_show_tmp = false;
}
void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
{
    gl_state gl;
    gl.save();

    m_mixer.bindFramebuffer();

    apply_canvas_viewport(0, 0, m_mixer.getWidth(), m_mixer.getHeight());
    apply_canvas_capability(depth_test_state(), false);
    apply_canvas_capability(scissor_test_state(), true);
    apply_canvas_capability(blend_state(), false);

    apply_canvas_scissor(
        static_cast<std::int32_t>(bb_min.x),
        static_cast<std::int32_t>(bb_min.y),
        static_cast<std::int32_t>(bb_sz.x),
        static_cast<std::int32_t>(bb_sz.y));

    auto layer_index = m_current_layer_idx;
    for (int plane_index = 0; plane_index < 6; plane_index++)
    {
        if (!m_layers[layer_index]->m_visible ||
            m_layers[layer_index]->m_opacity == .0f ||
            !m_layers[layer_index]->face(plane_index))
            continue;

        //glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)m_mixer.getWidth() / m_mixer.getHeight(), 0.1f, 1000.f);
        auto plane_mvp_z = 
            glm::scale(glm::vec3(1, -1, 1)) *
            m_proj * m_mv *
            m_plane_transform[plane_index] *
            glm::translate(glm::vec3(0, 0, -1));

        m_sampler.bind(0);
        m_sampler.bind(1);
        m_sampler.bind(2);
        const auto& b = m_current_stroke->m_brush;
        ShaderManager::use(kShader::CompDraw);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        //ShaderManager::u_int(kShaderUniform::TexA, 0);
        ShaderManager::u_int(kShaderUniform::TexStroke, 1);
        ShaderManager::u_int(kShaderUniform::TexMask, 2);
        ShaderManager::u_vec2(kShaderUniform::Resolution, m_size);
        ShaderManager::u_int(kShaderUniform::TexPattern, 3);
        ShaderManager::u_float(kShaderUniform::Alpha, 1);
        ShaderManager::u_int(kShaderUniform::Lock, false/*m_layers[layer_index]->m_alpha_locked*/);
        ShaderManager::u_int(kShaderUniform::Mask, false/*m_smask_active*/);
        ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
        ShaderManager::u_int(kShaderUniform::UseDual, false);
        ShaderManager::u_int(kShaderUniform::UsePattern, false);
        ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
        ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
        set_active_texture_unit(0);
        m_layers[layer_index]->rtt(plane_index).bindTexture();
        set_active_texture_unit(1);
        m_tmp[plane_index].bindTexture();
        set_active_texture_unit(2);
        m_smask.rtt(plane_index).bindTexture();
        m_node->m_face_plane.draw_fill();
        m_smask.rtt(plane_index).unbindTexture();
        set_active_texture_unit(1);
        m_tmp[plane_index].unbindTexture();
        set_active_texture_unit(0);
        m_layers[layer_index]->rtt(plane_index).unbindTexture();
    }
    m_sampler.unbind();
    m_mixer.unbindFramebuffer();
    
    gl.restore();
}

std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d /*= false*/, glm::mat4 mv /*= glm::mat4(1)*/) const
{
    // intersect P with the current face to clip diverging points from the plane
    const auto unp_vp = zw(m_box);
    const auto unp_inv = glm::inverse(m_proj * m_mv);
    std::array<std::vector<vertex_t>, 6> ret;
    for (int i = 0; i < 6; i++)
    {
        struct ray_t {
            glm::vec3 o;
            glm::vec3 d;
            vertex_t v;
            ray_t(glm::vec3 o, glm::vec3 d, vertex_t v) : o(o), d(d), v(v) { }
        };
        std::vector<ray_t> rays;
        if (project_3d)
        {
            rays.reserve(B.size());
            for (auto const& b : B)
                rays.emplace_back(glm::vec3(0), b.pos, b);
        }
        else
        {
            auto P = poly_intersect(B.data(), B.data() + 4, m_plane_shape[i]);
            rays.reserve(P.size());
            for (auto const& p : P)
            {
                glm::vec3 ray_origin, ray_dir;
                auto clip_space = glm::vec2(p.pos.x, unp_vp.y - p.pos.y - 1.f) / unp_vp * 2.f - 1.f;
                auto wp0 = unp_inv * glm::vec4(clip_space, 0, 1);
                auto wp1 = unp_inv * glm::vec4(clip_space, .5, 1);
                ray_origin = xyz(wp0 / wp0.w);
                ray_dir = glm::normalize(xyz(wp1 / wp1.w) - ray_origin);
                rays.emplace_back(ray_origin, ray_dir, p);
            }
        }
        glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]);
        std::vector<vertex_t> face_ret;
        face_ret.reserve(rays.size());
        for (auto const& r : rays)
        {
            glm::vec3 hit;
            float hit_t;
            if (ray_intersect(r.o, r.d, m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i], hit, hit_t))
            {
                glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);

                //P[j].uvs2 = xy(P[j].pos) / glm::vec2(App::I->width, App::I->height);
                vertex_t v;
                v.pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width;
                v.pos.y = (plane_local.y * 0.5f + 0.5f) * m_height;

                // Black magic - BEWARE!
                // interpolation perspective correction, use the current camera projection to correct the interpolation
                // because the new shape will have z fixed with an ortho projection when drawn to the face
                // we need to imitate the same perspective as the once in the camera
                // see: https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/perspective-correct-interpolation-vertex-attributes
                auto hit_cam = mv * glm::vec4(hit, 1);
                v.pos.z = 0;
                v.pos.w = hit_cam.z;
                v.uvs = r.v.uvs * hit_cam.z;
                v.uvs2 = r.v.uvs2 * hit_cam.z;
                face_ret.emplace_back(v);
            }
            else
            {
                break;
            }
        }
        if (face_ret.size() >= 3)
            ret[i] = std::move(face_ret);
    }
    return ret;
}

glm::vec4 Canvas::stroke_draw_samples(
    int i,
    std::vector<vertex_t>& P,
    bool copy_stroke_destination)
{
    if (copy_stroke_destination)
    {
        set_active_texture_unit(1);
        m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
    }

    glm::vec2 bb_min(m_width, m_height);
    glm::vec2 bb_max(0, 0);
    for (int j = 0; j < P.size(); j++)
    {
        bb_min = glm::max({ 0, 0 }, glm::min(bb_min, xy(P[j].pos)));
        bb_max = glm::min({ m_width, m_height }, glm::max(bb_max, xy(P[j].pos)));
    }
    auto bb_sz = bb_max - bb_min;

    glm::vec2 pad(1);
    glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height });
    glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos));
    if (copy_stroke_destination)
    {
        copy_framebuffer_to_texture_2d(tex_pos.x, tex_pos.y, tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
    }

    if (P.size() == 4)
    {
        static vertex_t rect[6];
        rect[0] = P[0];
        rect[1] = P[1];
        rect[2] = P[2];
        rect[3] = P[0];
        rect[4] = P[2];
        rect[5] = P[3];
        m_brush_shape.update_vertices(rect, 6);
    }
    else if (P.size() == 3)
    {
        m_brush_shape.update_vertices(P.data(), (int)P.size());
    }
    else
    {
        P = triangulate_simple(P);
        m_brush_shape.update_vertices(P.data(), (int)P.size());
    }
    m_brush_shape.draw_fill();

    if (copy_stroke_destination)
    {
        set_active_texture_unit(1);
        m_tex[i].unbind();
    }

    return glm::vec4(tex_pos, tex_pos + tex_sz);
}

std::vector<Canvas::StrokeFrame> Canvas::stroke_draw_compute(Stroke& stroke) const
{
    std::vector<StrokeFrame> ret;
    StrokeSample prev = stroke.m_prev_sample;
    auto samples = stroke.compute_samples();
    std::array<vertex_t, 4> B = {
        vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} },
        vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} },
        vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} },
        vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} },
    };
    for (const auto& s : samples)
    {
        if (!s.valid())
            continue;

        ret.emplace_back();
        auto& f = ret.back();

        glm::vec2 dx_mix(prev.size * 0.5f, 0), dy_mix(0, prev.size * 0.5f);
        glm::vec2 off_mix[4] = {
            -dx_mix - dy_mix, // A - bottom-left
            -dx_mix + dy_mix, // B - top-left
            +dx_mix + dy_mix, // C - top-right
            +dx_mix - dy_mix, // D - bottom-right
        };
        // P is the initial square centered at the cursor location 
        glm::vec2 dx(s.size * 0.5f, 0), dy(0, s.size * 0.5f);
        glm::vec2 off[4] = {
            -dx - dy, // A - bottom-left
            -dx + dy, // B - top-left
            +dx + dy, // C - top-right
            +dx - dy, // D - bottom-right
        };

        glm::vec2 mixer_sz(App::I->width, App::I->height);
        glm::vec2 mixer_bb_min(mixer_sz);
        glm::vec2 mixer_bb_max(0, 0);
        for (int j = 0; j < 4; j++)
        {
            auto p = (xy(prev.pos) + App::I->zoom * s.scale * off_mix[j] * glm::orientate2(-s.angle));
            mixer_bb_min = glm::max({ 0, 0 }, glm::min(mixer_bb_min, p));
            mixer_bb_max = glm::min(mixer_sz, glm::max(mixer_bb_max, p));

            if (s.pos.z == 0.f)
            {
                B[j].pos = glm::vec4(xy(s.pos) + App::I->zoom * s.scale * off[j] * glm::orientate2(-s.angle) - glm::vec2(0, 1), 1, 1);
            }
            else
            {
                auto m = glm::inverse(glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 }));
                glm::vec3 off_3d = m * glm::vec4(off[j], 0, 1);
                B[j].pos = glm::vec4(s.pos + off_3d, 1.f);
            }
            B[j].uvs2 = p / mixer_sz;
        }
        
        f.m_mixer_rect = glm::vec4(glm::floor(mixer_bb_min), glm::ceil(mixer_bb_max - mixer_bb_min)) / App::I->zoom;
        f.col = glm::vec4(s.col, 1);
        f.flow = s.flow;
        f.opacity = s.opacity;
        if (s.pos.z == 0.f)
        {
            f.shapes = stroke_draw_project(B, false, m_mv);
        }
        else
        {
            auto m = glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 });
            f.shapes = stroke_draw_project(B, true, m);
        }

        prev = s;
    }
    return ret;
}

void Canvas::stroke_draw()
{
    if (!(m_current_stroke && m_current_stroke->has_sample()))
    {
        stroke_commit_timelapse();
        //stroke_draw_mix({ 0,0 }, { m_mixer.getWidth(), m_mixer.getHeight() });
        return;
    }

    m_dirty = true;
    std::array<bool, 6> merge_faces;

    const auto vp = query_canvas_viewport();
    const auto cc = query_canvas_clear_color();
    
    const auto& brush = m_current_stroke->m_brush;
    const auto& dual_brush = m_dual_stroke->m_brush;
    auto ortho_proj = glm::ortho(0.f, (float)m_width, 0.f, (float)m_height, -1.f, 1.f);

    apply_canvas_viewport(0, 0, m_width, m_height);

    m_sampler_brush.bind(0);
    m_sampler_nearest.bind(1);
    m_sampler_stencil.bind(2);
    m_sampler.bind(3);
    //m_sampler_linear.bind(5);

    glm::vec2 patt_scale = glm::vec2(brush->m_pattern_scale);
    if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
    if (brush->m_pattern_flipy) patt_scale.y *= -1.f;

    const auto stroke_feedback = canvas_destination_feedback_plan(m_width, m_height);
    const bool copy_stroke_destination = !stroke_feedback.reads_destination_color;

    apply_canvas_capability(blend_state(), false);
    ShaderManager::use(kShader::Stroke);
    ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
    if (copy_stroke_destination)
        ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg
    ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern
    ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer
    //ShaderManager::u_int(kShaderUniform::TexMixA, 4); // mixer
    ShaderManager::u_float(kShaderUniform::Opacity, brush->m_tip_opacity);
    ShaderManager::u_vec2(kShaderUniform::Resolution, { m_width, m_height });
    ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
    ShaderManager::u_float(kShaderUniform::PatternInvert, brush->m_pattern_invert);
    ShaderManager::u_float(kShaderUniform::PatternBright, brush->m_pattern_brightness);
    ShaderManager::u_float(kShaderUniform::PatternContrast, brush->m_pattern_contrast);
    ShaderManager::u_float(kShaderUniform::PatternDepth, brush->m_pattern_depth);
    ShaderManager::u_int(kShaderUniform::PatternBlendMode, brush->m_pattern_blend_mode);
    ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset);
    ShaderManager::u_int(kShaderUniform::UsePattern, brush->m_pattern_enabled && brush->m_pattern_eachsample);
    ShaderManager::u_float(kShaderUniform::MixAlpha, brush->m_tip_mix);
    ShaderManager::u_float(kShaderUniform::Wet, brush->m_tip_wet);
    ShaderManager::u_float(kShaderUniform::Noise, brush->m_tip_noise);
    ShaderManager::u_mat4(kShaderUniform::MVP, ortho_proj);

    // DRAW MAIN BRUSH

    set_active_texture_unit(0);
    brush->m_tip_texture->bind();
    set_active_texture_unit(2);
    brush->m_pattern_texture ?
        brush->m_pattern_texture->bind() :
        unbind_texture_2d();
    set_active_texture_unit(3);
    m_mixer.bindTexture();

    auto frames = stroke_draw_compute(*m_current_stroke);
    
    std::array<glm::vec4, 6> box_face = SIXPLETTE(glm::vec4(m_size, 0, 0));
    std::array<bool, 6> box_dirty = SIXPLETTE(false);
    glm::vec4 pad_color;
    for (auto& f : frames)
    {
        if (brush->m_tip_mix > 0.f)
        {
            stroke_draw_mix(xy(f.m_mixer_rect), zw(f.m_mixer_rect));
        }

        for (int i = 0; i < 6; i++)
        {
            auto& P = f.shapes[i];
            if (P.size() < 3)
                continue;
            m_dirty_face[i] = true;
            merge_faces[i] = true;
            box_dirty[i] = true;

            m_tmp[i].bindFramebuffer();
            
            ShaderManager::use(kShader::Stroke);
            ShaderManager::u_vec4(kShaderUniform::Col, f.col);
            ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
            ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
            auto box_sample = stroke_draw_samples(i, P, copy_stroke_destination);

            m_tmp[i].unbindFramebuffer();
            
            m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width));
            box_face[i] = box_union(box_face[i], box_sample);
            // TODO: maybe average color?
            pad_color = f.col;
        }
    }

    set_active_texture_unit(3);
    m_mixer.unbindTexture();
    set_active_texture_unit(0);
    brush->m_tip_texture->unbind();

    // pad stroke
    // In order to mitigate color bleeding at the edge of shapes in transparent layers
    // we need to fill the area around the stroke with the same color because by default
    // the transparent area may have black or other undefined color.
    // This step is only useful for previewing the stroke because on commit the dilate
    // algorithm fixes this issue.
    // NOTE: at the moment this works on the whole canvas, but it can be optimized 
    // to only affect the current dirty box. In this case it may be necessary to do this
    // work on documents that doesn't have the padding, so on document loading.
    ShaderManager::use(kShader::StrokePad);
    ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
    if (copy_stroke_destination)
    {
        set_active_texture_unit(1);
        ShaderManager::u_int(kShaderUniform::TexBG, 1);
    }
    for (int i = 0; i < 6; i++)
    {
        if (!box_dirty[i])
            continue;
        const auto& b = box_face[i];
        glm::vec2 box_size = zw(b) - xy(b);
        glm::vec2 pad = { 20, 20 }; // pixels padding
        glm::vec4 pad_box = {
            glm::max({0, 0}, xy(b) - pad) * 2.f / m_size - 1.f,
            glm::min(m_size, zw(b) + pad) * 2.f / m_size - 1.f
        };
        // B(xw)--(zw)C   box
        //   |  //  |     coordinates
        // A(xy)--(zy)D   mapping
        std::array<vertex_t, 6> pad_quad = {
            vertex_t({pad_box.x, pad_box.y}), // A
            vertex_t({pad_box.x, pad_box.w}), // B
            vertex_t({pad_box.z, pad_box.w}), // C
            vertex_t({pad_box.x, pad_box.y}), // A
            vertex_t({pad_box.z, pad_box.w}), // C
            vertex_t({pad_box.z, pad_box.y}), // D
        };
        m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());

        m_tmp[i].bindFramebuffer();
        if (copy_stroke_destination)
        {
            glm::vec2 o = glm::max({0, 0}, xy(b) - pad);
            glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
            m_tex[i].bind();
            if (sz.x > 0 && sz.y > 0)
                copy_framebuffer_to_texture_2d(o.x, o.y, o.x, o.y, sz.x, sz.y);
        }
        m_brush_shape.draw_fill();
        m_tmp[i].unbindFramebuffer();
    }
    if (copy_stroke_destination)
    {
        unbind_texture_2d();
    }

    // DRAW DUAL BRUSH

    if (brush->m_dual_enabled)
    {
        ShaderManager::use(kShader::Stroke);
        ShaderManager::u_int(kShaderUniform::UsePattern, false);
        ShaderManager::u_float(kShaderUniform::MixAlpha, 0);
        ShaderManager::u_float(kShaderUniform::Wet, 0);
        ShaderManager::u_float(kShaderUniform::Noise, 0);
        
        set_active_texture_unit(0);
        dual_brush->m_tip_texture ? 
            dual_brush->m_tip_texture->bind() :
            unbind_texture_2d();
        auto frames_dual = stroke_draw_compute(*m_dual_stroke);
        for (auto& f : frames_dual)
        {
            ShaderManager::u_vec4(kShaderUniform::Col, f.col);
            ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
            ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
            for (int i = 0; i < 6; i++)
            {
                auto& P = f.shapes[i];
                if (P.size() < 3)
                    continue;
                m_tmp_dual[i].bindFramebuffer();
                auto box_sample = stroke_draw_samples(i, P, copy_stroke_destination);
                m_tmp_dual[i].unbindFramebuffer();

                // this mode overflows the main brush boundries
                if (brush->m_dual_blend_mode == 0)
                    m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width));
            }
        }
    }

    m_sampler_brush.unbind();
    m_sampler_nearest.unbind();
    m_sampler_stencil.unbind();
    
    apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
    apply_canvas_clear_color(cc);

    if (m_commit_delayed)
    {
        m_show_tmp = false;
        m_commit_delayed = false;
        stroke_commit();
        m_current_stroke = nullptr;
    }
}
bool Canvas::point_trace(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir,
    glm::vec3& hit_pos, glm::vec2& fb_pos, glm::vec3& hit_normal, int& out_plane_id)
{
    point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir);
    glm::vec3 hit;
    float hit_t;
    for (int i = 0; i < 6; i++)
    {
        if (ray_intersect(ray_origin, ray_dir, m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i], hit, hit_t))
        {
            glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]);
            glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);
            if (glm::abs(plane_local.x) < 1.f && glm::abs(plane_local.y) < 1.f)
            {
                fb_pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width;
                fb_pos.y = (plane_local.y * 0.5f + 0.5f) * m_height;
                hit_pos = hit;
                hit_normal = m_plane_normal[i];
                out_plane_id = i;
                return true;
            }
            else continue;
        }
        else continue;
    }
    return false;
}
/*
bool Canvas::point_trace_plane(glm::vec2 loc, glm::vec3& hit_pos, glm::vec2& hit_fb_pos, int plane_id)
{
    auto ln = (loc / zw(m_box)) * 2.f - 1.f;
    auto p = m_plane_unproject[plane_id] * glm::vec4(ln, 1, 1);
    if (p.w <= 0)
    {

        return true;
    }
    return false;
}
*/
bool Canvas::point_trace_plane(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir,
    glm::vec3& hit_pos, glm::vec3& hit_normal, glm::vec2& hit_fb_pos, int plane_id)
{
    point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir);
    glm::vec3 hit;
    float hit_t;
    if (ray_intersect(ray_origin, ray_dir, m_plane_origin[plane_id],
                      m_plane_normal[plane_id], m_plane_tangent[plane_id], hit, hit_t))
    {
        glm::mat4 plane_camera = glm::lookAt(m_plane_origin[plane_id], m_plane_normal[plane_id], m_plane_tangent[plane_id]);
        glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);
        hit_pos = hit;
        hit_normal = m_plane_normal[plane_id];
        hit_fb_pos.x = -(plane_local.x * 0.5f - 0.5f);
        hit_fb_pos.y = (plane_local.y * 0.5f + 0.5f);
        return true;
    }
    return false;
}
void Canvas::point_unproject(glm::vec2 loc, glm::vec4 vp, glm::mat4 camera, glm::mat4 proj,
    glm::vec3& out_origin, glm::vec3& out_dir)
{
    auto clip_space = glm::vec2(loc.x, vp.w - loc.y - 1.f) / zw(vp) * 2.f - 1.f;
    auto inv = glm::inverse(proj * camera);
    auto wp0 = inv * glm::vec4(clip_space, 0, 1);
    auto wp1 = inv * glm::vec4(clip_space, .5, 1);
    out_origin = xyz(wp0 / wp0.w);
    out_dir = glm::normalize(xyz(wp1 / wp1.w) - out_origin);
};
void Canvas::point_unproject(glm::vec2 loc, glm::vec3& out_origin, glm::vec3& out_dir)
{
    auto clip_space = glm::vec2(loc.x, m_vp.w - loc.y - 1.f) / zw(m_vp) * 2.f - 1.f;
    auto inv = glm::inverse(m_proj * m_mv);
    auto wp0 = inv * glm::vec4(clip_space, 0, 1);
    auto wp1 = inv * glm::vec4(clip_space, .5, 1);
    out_origin = xyz(wp0 / wp0.w);
    out_dir = glm::normalize(xyz(wp1 / wp1.w) - out_origin);
}
glm::vec3 Canvas::point_trace(glm::vec2 loc)
{
    glm::vec3 ray_origin;
    glm::vec3 ray_dir;
    glm::vec3 hit_pos;
    glm::vec3 hit_normal;
    glm::vec2 fb_pos;
    int plane_id;
    if (point_trace(loc, ray_origin, ray_dir, hit_pos, fb_pos, hit_normal, plane_id))
        return hit_pos;
    return glm::vec3(0);
}
void Canvas::stroke_commit()
{
    if (!m_dirty || m_layers.empty())
        return;

    m_dirty = false;
    m_dirty_stroke = true; // new stroke ready for timelapse capture
    App::I->redraw = true;
    
    // save viewport and clear color states
    const auto vp = query_canvas_viewport();
    const auto cc = query_canvas_clear_color();
    auto blend = query_canvas_capability(blend_state());
    
    // allocate action to add to history
    auto action = new ActionStroke;
    action->was_saved = !m_unsaved;

    m_unsaved = true;
    App::I->title_update();

    // prepare common states
    apply_canvas_viewport(0, 0, m_width, m_height);
    apply_canvas_capability(blend_state(), false);

    const auto& b = m_current_stroke->m_brush;

    for (int i = 0; i < 6; i++)
    {
        //m_dirty_box[i] = glm::vec4(0, 0, m_width, m_height); // reset bounding box
        if (!m_dirty_face[i])
            continue; // no stroke on this face, skip it

        m_layers[m_current_layer_idx]->rtt(i).bindFramebuffer();
        
        // save image before commit
        glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
        action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
        m_layers[m_current_layer_idx]->rtt(i).readPixelsRgba8(
            static_cast<int>(m_dirty_box[i].x),
            static_cast<int>(m_dirty_box[i].y),
            static_cast<int>(box_sz.x),
            static_cast<int>(box_sz.y),
            action->m_image[i].get());
        
        action->m_box[i] = m_dirty_box[i];
        action->m_old_box[i] = m_layers[m_current_layer_idx]->box(i);
        action->m_old_dirty[i] = m_layers[m_current_layer_idx]->face(i);

        if (!m_layers[m_current_layer_idx]->m_alpha_locked)
        {
            auto& lbox = m_layers[m_current_layer_idx]->box(i);
            lbox = glm::vec4(
                glm::min(xy(m_dirty_box[i]), xy(lbox)),
                glm::max(zw(m_dirty_box[i]), zw(lbox))
            );
        }
        m_layers[m_current_layer_idx]->face(i) = true;

        // copy to tmp2 for layer blending
        set_active_texture_unit(0);
        m_tex2[i].bind();
        copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
        m_tex2[i].unbind();
        
        m_tmp[i].bindTexture();
        set_active_texture_unit(1);
        m_tex2[i].bind();
        m_sampler.bind(0);
        m_sampler_nearest.bind(1);
        m_sampler.bind(2);
        m_sampler.bind(3);
        m_sampler_stencil.bind(4);
        if (m_current_mode == kCanvasMode::Erase)
        {
            ShaderManager::use(kShader::CompErase);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_int(kShaderUniform::TexStroke, 1);
            ShaderManager::u_int(kShaderUniform::TexMask, 2);
            ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
            ShaderManager::u_float(kShaderUniform::Alpha, 1);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));

            set_active_texture_unit(0);
            m_tex2[i].bind();
            set_active_texture_unit(1);
            m_tmp[i].bindTexture();
            set_active_texture_unit(2);
            m_smask.rtt(i).bindTexture();
            m_plane.draw_fill();
            m_smask.rtt(i).unbindTexture();
            set_active_texture_unit(1);
            m_tmp[i].unbindTexture();
            set_active_texture_unit(0);
            m_tex2[i].unbind();
        }
        else
        {
            glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
            if (b->m_pattern_flipx) patt_scale.x *= -1.f;
            if (b->m_pattern_flipy) patt_scale.y *= -1.f;
            
            ShaderManager::use(kShader::CompDraw);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_int(kShaderUniform::TexStroke, 1);
            ShaderManager::u_int(kShaderUniform::TexMask, 2);
            ShaderManager::u_int(kShaderUniform::TexDual, 3);
            ShaderManager::u_int(kShaderUniform::TexPattern, 4);
            ShaderManager::u_vec2(kShaderUniform::Resolution, m_size);
            ShaderManager::u_float(kShaderUniform::Alpha, 1);
            ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
            ShaderManager::u_int(kShaderUniform::Lock, m_layers[m_current_layer_idx]->m_alpha_locked);
            ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
            ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
            ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled);
            ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode);
            ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
            ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample);
            ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
            ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert);
            ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness);
            ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast);
            ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth);
            ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
            ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset);

            set_active_texture_unit(0);
            m_tex2[i].bind();
            set_active_texture_unit(1);
            m_tmp[i].bindTexture();
            set_active_texture_unit(2);
            m_smask.rtt(i).bindTexture();
            set_active_texture_unit(3);
            if (b->m_dual_enabled) 
                m_tmp_dual[i].bindTexture();
            set_active_texture_unit(4);
            b->m_pattern_texture ?
                b->m_pattern_texture->bind() :
                unbind_texture_2d();
            m_plane.draw_fill();
            set_active_texture_unit(3);
            if (b->m_dual_enabled)
                m_tmp_dual[i].unbindTexture();
            set_active_texture_unit(2);
            m_smask.rtt(i).unbindTexture();
            set_active_texture_unit(1);
            m_tmp[i].unbindTexture();
            set_active_texture_unit(0);
            m_tex2[i].unbind();
        }
//        else
//        {
//            ShaderManager::use(kShader::StrokeLayer);
//            ShaderManager::u_int(kShaderUniform::TexBG, 1);
//            ShaderManager::u_int(kShaderUniform::Lock, m_layers[m_current_layer_idx]->m_alpha_locked);
//            ShaderManager::u_float(kShaderUniform::Alpha, b->m_tip_opacity);
//
//            ShaderManager::u_int(kShaderUniform::Tex, 0);
//            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
//            m_plane.draw_fill();
//            m_sampler.unbind();
//            m_sampler_bg.unbind();
//            m_tex2[i].unbind();
//            m_tmp[i].unbindTexture();
//        }

        // Dilate borders to avoid interpolation bleeding
        ShaderManager::use(kShader::StrokeDilate);
        ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
        ShaderManager::u_int(kShaderUniform::TexBG, 0);
        set_active_texture_unit(0);
        m_tex2[i].bind();
        copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
        m_plane.draw_fill();
        
        m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer();
    }
    
    // restore viewport and clear color states
    blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
    apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
    apply_canvas_clear_color(cc);
    set_active_texture_unit(0);
    
    // save history
    action->m_layer_idx = m_current_layer_idx;
    action->m_frame_idx = layer().m_frame_index;
    action->m_canvas = this;
    //action->m_stroke = std::move(m_current_stroke);
    ActionManager::add(action);
    stroke_commit_timelapse();
}

void Canvas::stroke_commit_timelapse()
{
    if (m_encoder && App::I->rec_running)
    {
        auto t_now = std::chrono::high_resolution_clock::now();
        float dt = std::chrono::duration<float>(t_now - m_disrty_stroke_time).count();
        if (dt > 2.f && m_dirty_stroke && App::I->rec_mutex.try_lock())
        {
            draw_merge(true);
            App::I->rec_mutex.unlock();
            App::I->rec_cv.notify_one();
            LOG("rec frame generated");
            m_dirty_stroke = false;
            m_disrty_stroke_time = std::chrono::steady_clock::now();
        }
    }
}

void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SIXPLETTE(false)*/)
{
    assert(App::I->is_render_thread());

    apply_canvas_viewport(0, 0, m_width, m_height);
    auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f);
    const auto& b = m_current_stroke->m_brush;

    const auto blend_gate = draw_merge_blend_gate_plan(
        m_width,
        m_height,
        m_layers,
        m_current_stroke ? m_current_stroke->m_brush.get() : nullptr);
    const bool use_blend = blend_gate.shader_blend;
    const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;

    // if not using shader blend, use gl rasterizer blend
    apply_canvas_capability(depth_test_state(), false);

    for (int plane_index = 0; plane_index < 6; plane_index++)
    {
        if (!faces[plane_index])
            continue;

        m_layers_merge.rtt(plane_index).bindFramebuffer();
        m_layers_merge.rtt(plane_index).clear({ 1, 1, 1, 0 });

        if (use_blend)
        {
            apply_canvas_capability(blend_state(), false);
            m_layers_merge.rtt(plane_index).clear();
        }
        else
        {
            if (draw_checkerboard)
            {
                ShaderManager::use(kShader::Checkerboard);
                ShaderManager::u_int(kShaderUniform::Colorize, false);
                ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
                m_plane.draw_fill();
            }
            apply_canvas_capability(blend_state(), true);
        }

        for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
        {
            if (!(m_show_tmp && m_current_layer_idx == layer_index) &&
                (!m_layers[layer_index]->m_visible ||
                    m_layers[layer_index]->m_opacity == .0f ||
                    !m_layers[layer_index]->face(plane_index)))
                continue;
            
            if (use_blend)
            {
                m_merge_rtt.bindFramebuffer();
                m_merge_rtt.clear();
            }

            if (m_current_stroke && m_current_mode == kCanvasMode::Erase && m_show_tmp && m_current_layer_idx == layer_index)
            {
                m_sampler.bind(0);
                m_sampler.bind(1);
                m_sampler.bind(2);

                ShaderManager::use(kShader::CompErase);
                ShaderManager::u_int(kShaderUniform::Tex, 0);
                ShaderManager::u_int(kShaderUniform::TexStroke, 1);
                ShaderManager::u_int(kShaderUniform::TexMask, 2);
                //ShaderManager::u_vec2(kShaderUniform::Resolution, zw(m_box) / zoom);
                ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
                //ShaderManager::u_int(kShaderUniform::Lock, m_layers[layer_index]->m_alpha_locked);
                ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
                ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).bindTexture();
                set_active_texture_unit(1);
                m_tmp[plane_index].bindTexture();
                set_active_texture_unit(2);
                m_smask.rtt(plane_index).bindTexture();
                m_plane.draw_fill();
                m_smask.rtt(plane_index).unbindTexture();
                set_active_texture_unit(1);
                m_tmp[plane_index].unbindTexture();
                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).unbindTexture();
            }
            else if (m_current_stroke && m_show_tmp && m_current_layer_idx == layer_index)
            {
                m_sampler.bind(0);
                m_sampler.bind(1);
                m_sampler.bind(2);
                m_sampler.bind(3);
                m_sampler_stencil.bind(4);

                glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
                if (b->m_pattern_flipx) patt_scale.x *= -1.f;
                if (b->m_pattern_flipy) patt_scale.y *= -1.f;

                ShaderManager::use(kShader::CompDraw);
                ShaderManager::u_int(kShaderUniform::Tex, 0);
                ShaderManager::u_int(kShaderUniform::TexStroke, 1);
                ShaderManager::u_int(kShaderUniform::TexMask, 2);
                ShaderManager::u_int(kShaderUniform::TexDual, 3);
                ShaderManager::u_vec2(kShaderUniform::Resolution, Canvas::I->m_size);
                ShaderManager::u_int(kShaderUniform::TexPattern, 4);
                ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
                ShaderManager::u_int(kShaderUniform::Lock, m_layers[layer_index]->m_alpha_locked);
                ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
                ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
                ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
                ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
                ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled);
                ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode);
                ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample);
                ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
                ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert);
                ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness);
                ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast);
                ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth);
                ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
                ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
                ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);

                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).bindTexture();
                set_active_texture_unit(1);
                m_tmp[plane_index].bindTexture();
                set_active_texture_unit(2);
                m_smask.rtt(plane_index).bindTexture();
                set_active_texture_unit(3);
                if (b->m_dual_enabled)
                    m_tmp_dual[plane_index].bindTexture();
                set_active_texture_unit(4);
                b->m_pattern_texture ?
                    b->m_pattern_texture->bind() :
                    unbind_texture_2d();
                m_plane.draw_fill();
                set_active_texture_unit(3);
                if (b->m_dual_enabled)
                    m_tmp_dual[plane_index].unbindTexture();
                set_active_texture_unit(2);
                m_smask.rtt(plane_index).unbindTexture();
                set_active_texture_unit(1);
                m_tmp[plane_index].unbindTexture();
                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).unbindTexture();
            }
            else
            {
                m_cam_fov < 20.f ? m_sampler_nearest.bind(0) : m_sampler.bind(0);
                ShaderManager::use(kShader::TextureAlpha);
                ShaderManager::u_int(kShaderUniform::Tex, 0);
                ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
                ShaderManager::u_int(kShaderUniform::Highlight, m_layers[layer_index]->m_hightlight);
                ShaderManager::u_mat4(kShaderUniform::MVP, ortho);

                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).bindTexture();
                m_plane.draw_fill();
                m_layers[layer_index]->rtt(plane_index).unbindTexture();
            }

            if (use_blend)
            {
                m_merge_rtt.unbindFramebuffer();
            }

            // draw the blended
            if (use_blend)
            {
                m_sampler.bind(0);
                m_sampler.bind(2);

                ShaderManager::use(kShader::TextureBlend);
                ShaderManager::u_int(kShaderUniform::Tex, 0);
                ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
                ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
                ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
                if (copy_blend_destination)
                {
                    m_sampler.bind(2);
                    ShaderManager::u_int(kShaderUniform::TexBG, 2);
                }

                set_active_texture_unit(0);
                m_merge_rtt.bindTexture();
                if (copy_blend_destination)
                {
                    set_active_texture_unit(2);
                    m_merge_tex.bind();
                    copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
                }

                m_plane.draw_fill();

                if (copy_blend_destination)
                {
                    set_active_texture_unit(2);
                    m_merge_tex.unbind();
                }
                set_active_texture_unit(0);
                m_merge_rtt.unbindTexture();
            }
        }

        set_active_texture_unit(2);
        m_merge_tex.bind();
        copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
        
        // draw the grid behind the layers using a temporary copy
        if (use_blend)
        {
            apply_canvas_capability(blend_state(), true);

            //draw the grid
            if (draw_checkerboard)
            {
                ShaderManager::use(kShader::Checkerboard);
                ShaderManager::u_int(kShaderUniform::Colorize, false);
                ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
                m_plane.draw_fill();
            }

            // draw the layers
            m_sampler.bind(0);
            set_active_texture_unit(0);
            m_merge_tex.bind();
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
            m_plane.draw_fill();
            m_merge_tex.unbind();
        }
        
        m_layers_merge.rtt(plane_index).unbindFramebuffer();
    }
}

void Canvas::stroke_update(glm::vec3 point, float pressure)
{
    m_current_stroke->add_point(point, pressure);
    if (m_dual_stroke)
        m_dual_stroke->add_point(point, pressure);
}
void Canvas::stroke_start(glm::vec3 point, float pressure)
{
    assert(App::I->is_render_thread());

    // need to commit this now before starting a new stroke
    if (m_current_stroke && m_commit_delayed)
    {
        m_show_tmp = false;
        m_commit_delayed = false;
        stroke_commit();
        m_current_stroke = nullptr;
        m_dual_stroke = nullptr;
    }
    
    m_pattern_offset = m_current_brush->m_pattern_rand_offset ?
        glm::vec2((rand()%1000)*0.001f, (rand()%1000)*0.001f) :
        glm::vec2(0);

    m_current_stroke = std::make_unique<Stroke>();
    m_current_stroke->m_camera.rot = m_cam_rot;
    m_current_stroke->m_camera.fov = m_cam_fov;
    if (m_current_mode == kCanvasMode::Line)
        m_current_stroke->m_filter_points = false;
    m_current_stroke->randomize_prng();
    m_current_stroke->start(m_current_brush);
    m_current_stroke->add_point(point, pressure);

    // Generate a brush for the dual-brush
    if (m_current_brush->m_dual_enabled)
    {
        auto dual_brush = std::make_shared<Brush>();
        dual_brush->m_tip_flow = m_current_brush->m_dual_flow;
        dual_brush->m_tip_opacity = m_current_brush->m_dual_opacity;
        dual_brush->m_tip_flipx = m_current_brush->m_dual_flipx;
        dual_brush->m_tip_flipy = m_current_brush->m_dual_flipy;
        dual_brush->m_tip_invert = m_current_brush->m_dual_invert;
        dual_brush->m_blend_mode = m_current_brush->m_dual_blend_mode;
        dual_brush->m_tip_randflipx = m_current_brush->m_dual_randflip;
        dual_brush->m_tip_randflipy = m_current_brush->m_dual_randflip;
        dual_brush->m_tip_size = m_current_brush->m_dual_size * m_current_brush->m_tip_size;
        dual_brush->m_tip_spacing = m_current_brush->m_dual_spacing;
        dual_brush->m_jitter_scatter = m_current_brush->m_dual_scatter;
        dual_brush->m_jitter_scatter_bothaxis = m_current_brush->m_dual_scatter_bothaxis;
        dual_brush->m_jitter_angle = m_current_brush->m_dual_rotate;
        dual_brush->m_tip_texture = m_current_brush->m_dual_texture;
        dual_brush->m_tip_aspect = m_current_brush->m_dual_aspect;
        m_dual_stroke = std::make_unique<Stroke>();
        m_dual_stroke->m_camera.rot = m_cam_rot;
        m_dual_stroke->m_camera.fov = m_cam_fov;
        m_dual_stroke->start(dual_brush);
        m_dual_stroke->add_point(point, pressure);
    }

    auto const& l = m_layers[m_current_layer_idx];
    for (int i = 0; i < 6; i++)
    {
        m_dirty_box[i] = glm::vec4(m_width, m_height, 0, 0); // reset bounding box
        m_dirty_face[i] = false;
        
        m_tmp[i].bindFramebuffer();
        m_tmp[i].clear({ 0, 0, 0, 0 });
        m_tmp[i].unbindFramebuffer();

        if (m_current_brush->m_dual_enabled)
        {
            m_tmp_dual[i].bindFramebuffer();
            m_tmp_dual[i].clear({ 0, 0, 0, 0 });
            m_tmp_dual[i].unbindFramebuffer();
        }
    }
    m_mixer.bindFramebuffer();
    m_mixer.clear();
    m_mixer.unbindFramebuffer();
    m_show_tmp = true;
}
void Canvas::layer_add(std::string name, std::shared_ptr<Layer> layer /*= nullptr*/, int index /*= 0*/)
{
    LOG("canvas layer_add %s", name.c_str());
    int idx = (int)m_layers.size();
    if (layer)
    {
        m_layers.insert(m_layers.begin() + index, layer);
    }
    else
    {
        m_layers.insert(m_layers.begin() + index, std::make_unique<Layer>());
        m_layers[index]->create(m_width, m_height, name);
    }
    m_current_layer_idx = index;
}

std::shared_ptr<Layer> Canvas::layer_with_id(uint32_t id)
{
    auto layer = std::find_if(m_layers.begin(), m_layers.end(), 
        [id](const auto& l) { return l->id == id; });
    if (layer != m_layers.end())
        return *layer;
    return nullptr;
}

void Canvas::layer_remove(int idx) // m_order index
{
    LOG("canvas layer_remove %d", idx);
    //m_layers[n]->destroy();
    m_layers.erase(m_layers.begin() + idx);
    m_current_layer_idx = std::min<int>((int)m_layers.size() - 1, idx);
}
void Canvas::layer_order(int idx, int pos) // m_order index
{
    auto from = m_layers.begin() + idx;
    auto to = m_layers.begin() + pos;

    if (pos < idx)
        std::rotate(to, from, from + 1);
    if (pos > idx)
        std::rotate(from, from + 1, to + 1);

    if (m_current_layer_idx == idx)
        m_current_layer_idx = pos;
    else if (m_current_layer_idx == pos)
        m_current_layer_idx = idx;
}
void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
{
    m_dirty = false;

    App::I->render_task([&]
    {
        // prepare common states
        apply_canvas_viewport(0, 0, m_width, m_height);
        apply_canvas_capability(blend_state(), false);

        for (int i = 0; i < 6; i++)
        {
            if (!m_layers[source_idx]->face(i))
                continue; // no stroke on this face, skip it

            m_layers[dest_idx]->rtt(i).bindFramebuffer();

            auto& lbox = m_layers[dest_idx]->box(i);
            lbox = glm::vec4(
                glm::min(xy(m_layers[source_idx]->box(i)), xy(lbox)),
                glm::max(zw(m_layers[source_idx]->box(i)), zw(lbox))
            );
            m_layers[dest_idx]->face(i) = true;

            // copy to tmp2 for layer blending
            set_active_texture_unit(0);
            m_tex2[i].bind();
            copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
            m_tex2[i].unbind();

            m_sampler.bind(0);
            m_sampler_nearest.bind(1);
            {
                ShaderManager::use(kShader::CompDraw);
                ShaderManager::u_int(kShaderUniform::Tex, 0); // dest
                ShaderManager::u_int(kShaderUniform::TexStroke, 1); // source
                ShaderManager::u_vec2(kShaderUniform::Resolution, m_size);
                ShaderManager::u_float(kShaderUniform::Alpha, m_layers[source_idx]->m_opacity);
                ShaderManager::u_int(kShaderUniform::Lock, false);
                ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
                ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[source_idx]->m_blend_mode);
                ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
                ShaderManager::u_int(kShaderUniform::UseDual, false);
                ShaderManager::u_int(kShaderUniform::UsePattern, false);

                set_active_texture_unit(0);
                m_tex2[i].bind();
                set_active_texture_unit(1);
                m_layers[source_idx]->rtt(i).bindTexture();
                m_plane.draw_fill();
                m_layers[source_idx]->rtt(i).unbindTexture();
                set_active_texture_unit(0);
                m_tex2[i].unbind();
            }

            m_layers[dest_idx]->rtt(i).unbindFramebuffer();
        }
    });
}

int Canvas::anim_duration() const noexcept
{
    int frames = 1;
    for (auto& l : m_layers)
        frames = glm::max(frames, l->total_duration());
    return frames;
}

void Canvas::anim_update() noexcept
{
    for (auto& l : m_layers)
        l->goto_frame(m_anim_frame);
}

void Canvas::anim_goto_frame(int frame) noexcept
{
    m_anim_frame = frame;
    for (auto& l : m_layers)
        l->goto_frame(frame);
}

void Canvas::anim_goto_next() noexcept
{
    anim_goto_frame((m_anim_frame + 1) % anim_duration());
}

void Canvas::anim_goto_prev() noexcept
{
    int k = anim_duration();
    anim_goto_frame((m_anim_frame - 1 + k) % k);
}

void Canvas::flood_fill(int layer, int plane, std::vector<glm::ivec2> pos, FloodData& plane_data,
    float threshold, glm::vec4 dest_color, std::unique_ptr<glm::vec4>& source_color)
{
    struct adj_t
    {
        int plane;
        bool flipx;
        bool flipy;
        bool flipcoord;
        adj_t(int plane, bool flipx, bool flipy, int flipcoord) :
            plane(plane), flipx(flipx), flipy(flipy), flipcoord(flipcoord) { }
        glm::ivec2 compute(glm::ivec2 p, glm::ivec2 sz) const
        {
            glm::ivec2 ret;
            ret[flipcoord] = flipx ? sz.x - p.x : p.x;
            ret[1 - flipcoord] = flipy ? sz.y - p.y : p.y;
            return ret;
        }
    };

    LOG("flood_fill plane %d", plane);

    auto& rtt = m_layers[layer]->rtt(plane);
    auto sz = rtt.getSize();
    
    if (!plane_data.mask[plane])
    {
        plane_data.mask[plane] = std::make_unique<bool[]>((size_t)sz.x * sz.y);
        plane_data.rgb[plane] = std::unique_ptr<glm::u8vec4[]>(
            reinterpret_cast<glm::u8vec4*>(m_layers[layer]->rtt(plane).readTextureData()));
        plane_data.bb[plane] = { sz.x, sz.y, 0, 0 };
        plane_data.dirty[plane] = false;
        plane_data.layer = m_layers[layer];
    }
    auto& mask = plane_data.mask[plane];
    auto& rgb = plane_data.rgb[plane];

    if (!source_color)
        source_color = std::make_unique<glm::vec4>(rgb[pos.back().y * sz.x + pos.back().x]);
    const glm::vec4 c = *source_color;

    std::array<std::vector<glm::ivec2>, 4> edges;
    static const std::array<adj_t, 4> adj[6] = {
        // front
        {
            adj_t(3, 1, 0, 0),
            adj_t(4, 1, 1, 0),
            adj_t(1, 0, 0, 0),
            adj_t(5, 1, 0, 0),
        },
        // right
        {
            adj_t(0, 1, 0, 0),
            adj_t(4, 1, 0, 1),
            adj_t(2, 0, 0, 0),
            adj_t(5, 0, 0, 1),
        },
        // back
        {
            adj_t(1, 1, 0, 0),
            adj_t(4, 0, 0, 0),
            adj_t(3, 0, 0, 0),
            adj_t(5, 0, 1, 0),
        },
        // left
        {
            adj_t(2, 1, 0, 0),
            adj_t(4, 0, 1, 1),
            adj_t(0, 0, 0, 0),
            adj_t(5, 1, 1, 1),
        },
        // top
        {
            adj_t(1, 1, 1, 1),
            adj_t(0, 1, 1, 0),
            adj_t(3, 1, 0, 1),
            adj_t(2, 0, 1, 0),
        },
        // bottom
        {
            adj_t(1, 0, 0, 1),
            adj_t(2, 0, 0, 0),
            adj_t(3, 0, 1, 1),
            adj_t(0, 1, 0, 0),
        },
    };

    auto test = [&](glm::ivec2 p, bool set_color) -> bool
    {
        int i = p.y * sz.x + p.x;
        if (p.x < 0)
        {
            edges[0].push_back(adj[plane][0].compute({ -p.x, p.y }, sz));
            return false;
        }
        else if (p.x >= sz.x)
        {
            edges[2].push_back(adj[plane][2].compute({ sz.x - p.x + 1, p.y }, sz));
            return false;
        }
        else if (p.y < 0)
        {
            edges[3].push_back(adj[plane][3].compute({ p.x, -p.y }, sz));
            return false;
        }
        else if (p.y >= sz.y)
        {
            edges[1].push_back(adj[plane][1].compute({  p.x, sz.y - p.y + 1 }, sz));
            return false;
        }
        if (!mask[i])
        {
            if (c.a == 0 && glm::abs(rgb[i].a - c.a) < threshold || 
                c.a > 0 && rgb[i].a > 0 && glm::distance(glm::vec3(c), glm::vec3(rgb[i])) < threshold)
            {
                if (set_color)
                {
                    mask[i] = true;
                    rgb[i] = dest_color * 255.f;
                    plane_data.dirty[plane] = true;
                    glm::vec2 bb_min = glm::min((glm::vec2)p, xy(plane_data.bb[plane]));
                    // add 1 pixel to the end because 1 pixel has min(0) and max(1)
                    glm::vec2 bb_max = glm::max((glm::vec2)p + glm::vec2(1), zw(plane_data.bb[plane]));
                    plane_data.bb[plane] = { bb_min, bb_max };
                }
                pos.push_back(p);
            }
            return true;
        }
        return false;
    };

    while (!pos.empty())
    {
        auto p = pos.back();
        pos.pop_back();
        if(!test(p + glm::ivec2( 0, 0), true))
            continue;
        test(p + glm::ivec2(-1, 0), false);
        test(p + glm::ivec2(1, 0), false);
        test(p + glm::ivec2(0, 1), false);
        test(p + glm::ivec2(0, -1), false);
        //for (int x = -1; x <= 1; x++)
        //    for (int y = -1; y <= 1; y++)
        //        if (x != 0 && y != 0)
        //            test(p + glm::ivec2(x, y), false);
    }

    for (int i = 0; i < 4; i++)
    {
        if (!edges[i].empty())
        {
            flood_fill(layer, adj[plane][i].plane, edges[i], plane_data, threshold, dest_color, source_color);
            //LOG("continue to plane %d -> %d", plane, adj[plane][i].plane);
        }
    }
}

void Canvas::FloodData::apply()
{
    for (int plane = 0; plane < 6; plane++)
    {
        if (!dirty[plane])
            continue;
        auto& rtt = layer->rtt(plane);
        App::I->render_task([&]
        {
            rtt.updateRgba8(0, 0, rtt.getWidth(), rtt.getHeight(), rgb[plane].get());
        });
        layer->face(plane) = true;
        layer->box(plane) = box_union(layer->box(plane), bb[plane]);
    }
    Canvas::I->m_unsaved = true;
}

void Canvas::resize(int width, int height)
{
    m_width = width;
    m_height = height;
    m_size = { width, height };
    for (int i = 0; i < 6; i++)
    {
        const auto stroke_format = current_canvas_stroke_internal_format();
        m_tmp[i].create(width, height, -1, stroke_format);
        m_tmp_dual[i].create(width, height, -1, stroke_format);
        m_tex[i].create(width, height, rgba8_internal_format());
        m_tex2[i].create(width, height, rgba8_internal_format());
    }
    for (auto& l : m_layers)
        l->resize(width, height);
    m_smask.create(width, height, "mask");
    m_layers_merge.resize(width, height);
    m_merge_rtt.create(width, height);
    m_merge_tex.create(width, height);
    m_unsaved = true;
}

void Canvas::destroy()
{
    for (int i = 0; i < 6; i++)
    {
        m_tmp[i].destroy();
        m_tmp_dual[i].destroy();
        m_tex[i].destroy();
        m_tex2[i].destroy();
    }
    for (auto& l : m_layers)
        l->destroy();
    m_smask.destroy();
    m_mixer.destroy();
    m_layers_merge.destroy();
    m_merge_rtt.destroy();
    m_merge_tex.destroy();
}

bool Canvas::create(int width, int height)
{
    m_width = width;
    m_height = height;
    m_size = { width, height };
    for (int i = 0; i < 6; i++)
    {
        const auto stroke_format = current_canvas_stroke_internal_format();
        m_tmp[i].create(width, height, -1, stroke_format);
        m_tmp_dual[i].create(width, height, -1, stroke_format);
        m_tex[i].create(width, height, rgba8_internal_format());
        m_tex2[i].create(width, height, rgba8_internal_format());
    }
#if defined(__GLES__)
    m_sampler_brush.create();
#else
    m_sampler_brush.create(texture_filter_linear(), texture_wrap_clamp_to_border());
#endif
    m_sampler.create(texture_filter_linear());
    m_sampler_nearest.create(texture_filter_nearest());
    m_sampler_brush.set_filter(texture_filter_linear_mipmap_linear(), texture_filter_linear());
    m_sampler_brush.set_border({ 1, 1, 1, 1 });
    m_sampler_stencil.create(texture_filter_linear(), texture_wrap_repeat());
    m_sampler_mix.create(texture_filter_nearest(), texture_wrap_repeat());
    m_sampler_linear.create();
    m_plane.create<1>(1, 1);
    m_plane_brush.create<1>(1, 1);
    m_brush_shape.create();
    for (auto& l : m_layers)
        l->create(width, height, "");
    m_smask.create(width, height, "mask");
    //m_smask.clear({1, 1, 1, 1});
    m_layers_merge.create(width, height, "merge");
    m_merge_rtt.create(width, height);
    m_merge_tex.create(width, height);
    m_unsaved = true;

    timelapse_reset_encoder();

    return true;
}

void Canvas::snapshot_save()
{
    LOG("SAVE SNAPSHOT");
    m_layers_snapshot.clear();
    m_layers_snapshot.resize(m_layers.size());
    for (int i = 0; i < m_layers.size(); i++)
        m_layers_snapshot[i] = m_layers[i]->snapshot();
}

void Canvas::snapshot_restore()
{
    LOG("RESTORE SNAPSHOT");
    for (int i = 0; i < m_layers.size(); i++)
        m_layers[i]->restore(m_layers_snapshot[i]);
    m_layers_snapshot.clear();
}

void Canvas::clear_context()
{
    LOG("Canvas CLEAR CONTEXT");
    for (auto& layer : m_layers)
        layer->destroy();
    for (int i = 0; i < 6; i++)
    {
        m_tmp[i].destroy();
        m_tex[i].destroy();
        m_tex2[i].destroy();
    }
};

void Canvas::import_equirectangular(std::string file_path, std::shared_ptr<Layer> layer /*= nullptr*/)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            import_equirectangular_thread(file_path, layer);
        });
        t.detach();
    }
}

void Canvas::import_equirectangular_thread(std::string file_path, std::shared_ptr<Layer> layer /*= nullptr*/, int frame /*= -1*/)
{
    Image img;
    if (!img.load_file(file_path))
        return;

    if (!layer)
        layer = m_layers[m_current_layer_idx];

    if (frame == -1)
        frame = layer->m_frame_index;
    
    auto a = new ActionImportEquirect;
    a->m_layer = layer;
    a->m_frame = frame;
    a->m_snap = std::make_shared<LayerFrame::Snapshot>(layer->snapshot(frame));
    a->m_path = file_path;
    ActionManager::add(a);

    m_unsaved = true;

    if (img.width == img.height / 6)
    {
        Texture2D tex;
        static const GLint indices[] = { 5, 0, 4, 1, 2, 3 };
        const auto texture_format = texture_format_for_image_channels(img.comp);
        tex.create(
            img.width,
            img.width,
            static_cast<GLint>(texture_format.internal_format),
            static_cast<GLint>(texture_format.pixel_format));
        int stride = img.width * img.width * img.comp;
        Plane plane;
        plane.create<1>(2, 2);
        draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
            apply_canvas_capability(depth_test_state(), false);
            tex.update(img.m_data.get() + indices[i] * stride);
            m_sampler.bind(0);
            set_active_texture_unit(0);
            tex.bind();
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::scale(glm::vec3(-1, -1, 1)));
            plane.draw_fill();
            tex.unbind();
            m_sampler.unbind();
        }, frame, false);
        plane.destroy();
    }
    else
    {
        Texture2D tex;
        tex.load_file(file_path);
        Sphere sphere;
        sphere.create<64, 64>(2.f);
        draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
            apply_canvas_capability(depth_test_state(), false);
            m_sampler.bind(0);
            set_active_texture_unit(0);
            tex.bind();
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera *
                glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1)));
            sphere.draw_fill();
            tex.unbind();
            m_sampler.unbind();
        }, frame, false);
        sphere.destroy();
    }
    for (int i = 0; i < 6; i++)
    {
        layer->box(i) = glm::vec4(0, 0, m_width, m_height);
        layer->face(i) = true;
    }
}

void Canvas::export_equirectangular(std::string file_path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_equirectangular_thread(file_path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_equirectangular_thread(std::string file_path)
{
    Image data;
    
    App::I->render_task([&]
    {
        draw_merge(false);
        Texture2D equirect = m_layers_merge.gen_equirect();
        data = equirect.get_image();
    });

    LOG("writing %s", file_path.c_str());
    if (file_path.substr(file_path.size() - 4) == ".jpg")
    {
        data.save_jpg(file_path, 100);
        inject_xmp(file_path);
    }
    else if (file_path.substr(file_path.size() - 4) == ".png")
    {
        data.save_png(file_path);
    }

    App::I->publish_exported_image(file_path);
}

void Canvas::inject_xmp(std::string jpg_path)
{
    static const char xmp[] = 
"http://ns.adobe.com/xap/1.0/\0" R"(<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
  <x:xmpmeta xmlns:x="adobe:ns:meta/" xmptk="SAMSUNG 360CAM">
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about="" xmlns:GPano="http://ns.google.com/photos/1.0/panorama/">
        <GPano:ProjectionType>equirectangular</GPano:ProjectionType>
        <GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>
        <GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>
        <GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>
        <GPano:PoseHeadingDegrees>0</GPano:PoseHeadingDegrees>
        <GPano:PosePitchDegrees>0</GPano:PosePitchDegrees>
        <GPano:PoseRollDegrees>0</GPano:PoseRollDegrees>
        <GPano:StitchingSoftware>PanoPainter</GPano:StitchingSoftware>
      </rdf:Description>
    </rdf:RDF>
  </x:xmpmeta>
<?xpacket end="r"?>)";

    FILE* fp = fopen(jpg_path.c_str(), "rb");
    fseek(fp, 0, SEEK_END);
    long len = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    unsigned char* jpeg_data = (unsigned char*)malloc(len);
    fread(jpeg_data, len, 1, fp);
    fclose(fp);

    fp = fopen(jpg_path.c_str(), "wb");

    int i = 0;
    while (i < len && !(jpeg_data[i] == 0xff && jpeg_data[i + 1] == 0xd8)) i++;
    i += 2;

    unsigned char* xmp_section = (unsigned char*)malloc(sizeof(xmp) + 4);
    xmp_section[0] = 0xff;
    xmp_section[1] = 0xe1;
    xmp_section[2] = ((int)sizeof(xmp) + 2) >> 8;
    xmp_section[3] = ((int)sizeof(xmp) + 2) >> 0;
    memcpy(xmp_section + 4, xmp, sizeof(xmp));

    fwrite(jpeg_data, 1, i, fp);
    fwrite(xmp_section, 1, sizeof(xmp) + 4, fp);
    fwrite(jpeg_data + i, 1, len - i, fp);
    fclose(fp);

}

void Canvas::export_depth(std::string file_name, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_depth_thread(file_name);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_depth_thread(std::string file_name)
{
    RTT rtt;
    rtt.create(1024, 1024);

    glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)rtt.getWidth() / (float)rtt.getHeight(), 0.1f, 100.f);
    glm::mat4 camera = m_cam_rot;

    App::I->render_task([&]
    {
        draw_merge(false);

        rtt.bindFramebuffer();
        rtt.clear({ 0, 0, 0, 1 });
        apply_canvas_capability(blend_state(), true);
        apply_canvas_capability(depth_test_state(), false);
        apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight());
        for (int plane_index = 0; plane_index < 6; plane_index++)
        {
            auto plane_mvp_z = proj * camera *
                m_plane_transform[plane_index] *
                glm::translate(glm::vec3(0, 0, -1)) *
                glm::scale(glm::vec3(2));

            m_sampler.bind(0);
            ShaderManager::use(kShader::TextureAlpha);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
            ShaderManager::u_int(kShaderUniform::Highlight, false);
            ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);

            set_active_texture_unit(0);
            m_layers_merge.rtt(plane_index).bindTexture();
            m_plane.draw_fill();
            m_layers_merge.rtt(plane_index).unbindTexture();
        }
        rtt.unbindFramebuffer();
    });

    uint8_t* rgba_data = rtt.readTextureData();
    stbi_flip_vertically_on_write(true);
    std::string path_rgba = App::I->work_path + "/" + file_name + ".png";
    stbi_write_jpg(path_rgba.c_str(), rtt.getWidth(), rtt.getHeight(), 4, rgba_data, 100);
    delete rgba_data;

    App::I->render_task([&]
    {
        rtt.bindFramebuffer();
        rtt.clear({ 0, 0, 0, 1 });
        apply_canvas_capability(blend_state(), true);
        apply_canvas_capability(depth_test_state(), false);
        apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight());
        for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
        {
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                if ((!m_layers[layer_index]->m_visible ||
                    m_layers[layer_index]->m_opacity == .0f ||
                    !m_layers[layer_index]->face(plane_index)))
                    continue;

                auto plane_mvp_z = proj * camera *
                    m_plane_transform[plane_index] *
                    glm::translate(glm::vec3(0, 0, -1)) *
                    glm::scale(glm::vec3(2));

                m_sampler.bind(0);
                ShaderManager::use(kShader::TextureColorize);
                ShaderManager::u_int(kShaderUniform::Tex, 0);
                ShaderManager::u_vec4(kShaderUniform::Col, { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f });
                ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);

                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).bindTexture();
                m_plane.draw_fill();
                m_layers[layer_index]->rtt(plane_index).unbindTexture();
            }
        }
        rtt.unbindFramebuffer();
    });

    uint8_t* depth_data = rtt.readTextureData();
    std::string path_depth = App::I->work_path + "/" + file_name + "_depth.png";
    stbi_write_jpg(path_depth.c_str(), rtt.getWidth(), rtt.getHeight(), 4, depth_data, 100);
    delete depth_data;
    stbi_flip_vertically_on_write(false);

    rtt.destroy();
}

void Canvas::export_layers(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_layers_thread(path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_layers_thread(std::string path)
{
    auto pb = App::I->show_progress("Export Layers", m_layers.size());
    for (int i = 0; i < m_layers.size(); i++)
    {
        auto l = m_layers[i];
        Image img = l->gen_equirect().get_image();
        img.save_png(fmt::format("{}-layer{:02d}-{}.png", path, i, l->m_name));
        pb->increment();
    }
    pb->destroy();
}

void Canvas::export_anim_frames(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_anim_frames_thread(path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_anim_frames_thread(std::string path)
{
    auto pb = App::I->show_progress("Export Frames", anim_duration());
    for (int i = 0; i < anim_duration(); i++)
    {
        anim_goto_frame(i);
        export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i));
        pb->increment();
    }
    pb->destroy();
}

void Canvas::export_anim_mp4(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_anim_mp4_thread(path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_anim_mp4_thread(std::string path)
{
    auto pb = App::I->show_progress("Export Animation", anim_duration());
    int fps = App::I->animation->get_fps();
    MP4Encoder mp4;
    int res = std::min<int>(1024, m_width);
    mp4.init(res * 4, res * 2, 30, 2 << 20);
    for (int i = 0; i < anim_duration(); i++)
    {
        Image data;
        App::I->render_task([&]
        {
            anim_goto_frame(i);
            draw_merge(false);
            Texture2D equirect = m_layers_merge.gen_equirect({ res, res });
            data = equirect.get_image();
        });
        for (int j = 0; j < 30/fps; j++)
            mp4.encode(data);
        pb->increment();
    }
    mp4.write_mp4(path);
    pb->destroy();
}

void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_cube_faces_thread(file_name);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_cube_faces_thread(std::string file_name)
{
#ifdef __OBJC__
    NSMutableArray* files = [NSMutableArray array];
#endif
    static std::array<const char*, 6> plane_names{ "front", "right", "back", "left", "top", "bottom" };

    auto pb = App::I->show_progress("Export Cube Faces", 7);
    
    App::I->render_task([this] {
        draw_merge(false);
    });
    pb->increment();

    for (int i = 0; i < 6; i++)
    {
        Image face = m_layers_merge.rtt(i).get_image();
        std::string path = fmt::format("{}/{}-{}.png", App::I->work_path, file_name, plane_names[i]);
        face.save_png(path);
        pb->increment();

        App::I->publish_exported_image(path);
#ifdef __OBJC__
        [files addObject : [NSString stringWithUTF8String:path.c_str()] ];
#endif
    }

    pb->destroy();

#ifdef __OBJC__
    static char name[128];
    sprintf(name, "%s.zip", App::I->work_path.c_str());
    auto zip_path = [NSString stringWithUTF8String:name];
    //[SSZipArchive createZipFileAtPath:zip_path withFilesAtPaths:files];
    //for (NSString* f : files)
    //    [[NSFileManager defaultManager] removeItemAtPath:f error:nil];
#endif
}

void Canvas::project_save(std::function<void(bool)> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            bool ret = project_save_thread(App::I->doc_path, true);
            if (on_complete)
                on_complete(ret);
        });
        t.detach();
    }
}

void Canvas::project_save(std::string file_path, std::function<void(bool)> on_complete)
{
    LOG("saving %s", file_path.c_str());
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            bool ret = project_save_thread(file_path, true);
            if (on_complete)
                on_complete(ret);
        });
        t.detach();
    }
    else
    {
        LOG("no license, no save");
    }
}

bool Canvas::project_save_thread(std::string file_path, bool show_progress)
{
    // already saved, nothing to do
    if (!m_unsaved && file_path == App::I->doc_path)
    {
        LOG("already saved");
        return true;
    }

//     static char name[128];
//     sprintf(name, "%s/latlong.ppi", data_path.c_str());
    FILE* fp;
    
    auto start = file_path.rfind('/') + 1;
    std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi"));
    std::string tmp_path = App::I->data_path + '/' + file_name + ".tmp.ppi";
    std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl";
    
    LOG("file name %s", file_name.c_str());
    LOG("tmp path %s", tmp_path.c_str());

    bool use_tmp = false;
    // check if file already exists
    if ((fp = fopen(file_path.c_str(), "rb")))
    {
        fclose(fp);
        LOG("use tmp file");
        // use tmp file for writing
        use_tmp = true;
        if (!(fp = fopen(tmp_path.c_str(), "wb")))
        {
            LOG("cannot write tmp project to %s", tmp_path.c_str());
            use_tmp = false;
        }
    }

    LOG("save first time");
    
    if (!fp)
    {
        // write directly to the new file
        if (!(fp = fopen(file_path.c_str(), "wb")))
        {
            LOG("cannot write project to %s", file_path.c_str());
            return false;
        }
        LOG("unsafe mode saving directly to %s", file_path.c_str());
    }

    PPIHeader ppi_header;
    fwrite(&ppi_header, sizeof(PPIHeader), 1, fp);

    // load thumbnail
    Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height);
    
    std::shared_ptr<NodeProgressBar> pb;
    if (show_progress)
        pb = App::I->show_progress("Saving Pano Project");
    
    thumb.flip();
    fwrite(thumb.data(), thumb.size(), 1, fp);

    fwrite(&m_width, sizeof(int), 1, fp);
    fwrite(&m_height, sizeof(int), 1, fp);
    
    int n_layers = (int)m_layers.size();
    fwrite(&n_layers, sizeof(int), 1, fp);
    
    int n_frames = std::accumulate(m_layers.begin(), m_layers.end(), 0, 
        [](int tot, auto& l) { return tot + l->frames_count(); });
    if (ppi_header.doc_version.minor >= 3)
        fwrite(&n_frames, sizeof(int), 1, fp);

    int progress = 0;
    int total = n_frames * 6;
    
    for (int i = 0; i < (int)m_layers.size(); i++)
    {
        int n_order = i;
        fwrite(&n_order, sizeof(int), 1, fp);

        float layer_alpha = m_layers[i]->m_opacity;
        fwrite(&layer_alpha, sizeof(float), 1, fp);

        int name_len = (int)m_layers[i]->m_name.size();
        fwrite(&name_len, sizeof(int), 1, fp);
        fwrite(m_layers[i]->m_name.data(), name_len, 1, fp);

        if (ppi_header.doc_version.minor >= 2)
        {
            fwrite(&m_layers[i]->m_blend_mode, sizeof(int), 1, fp);
            fwrite(&m_layers[i]->m_alpha_locked, sizeof(bool), 1, fp);
            fwrite(&m_layers[i]->m_visible, sizeof(bool), 1, fp);
        }

        int frames = 1;
        if (ppi_header.doc_version.minor >= 3)
        {
            frames = (int)m_layers[i]->frames_count();
            fwrite(&frames, sizeof(int), 1, fp);
        }
        
        for (int fi = 0; fi < frames; fi++)
        {
            if (ppi_header.doc_version.minor >= 3)
            {
                int duration = m_layers[i]->frame_duration(fi);
                fwrite(&duration, sizeof(int), 1, fp);
            }
            bool gpu = m_layers[i]->frame(fi).gpu_load();
            m_layers[i]->optimize(fi);
            auto snap = m_layers[i]->snapshot(fi);
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                int has_data = snap.m_dirty_face[plane_index] ? 1 : 0;
                fwrite(&has_data, sizeof(int), 1, fp);
                if (has_data)
                {
                    glm::ivec4 b = snap.m_dirty_box[plane_index];
                    glm::vec2 sz = zw(b) - xy(b);
                    int box[4] = { b.x, b.y, b.z, b.w };
                    fwrite(&box, sizeof(box), 1, fp);

                    std::vector<uint8_t> compressed;
                    auto callback = [](void* context, void* data, int size)
                    {
                        std::vector<uint8_t>* buffer = static_cast<std::vector<uint8_t>*>(context);
                        buffer->insert(buffer->end(), (uint8_t*)data, (uint8_t*)data + size);
                    };
                    int ret = stbi_write_png_to_func(callback, &compressed, sz.x, sz.y, 4, snap.image[plane_index].get(), sz.x * 4);

                    int data_size = (int)compressed.size();
                    fwrite(&data_size, sizeof(int), 1, fp);

                    fwrite(compressed.data(), 1, compressed.size(), fp);
                }
                progress++;
                float p = (float)progress / total * 100.f;
                if (show_progress)
                    pb->m_progress->SetWidthP(p);
                LOG("progress: %f", p);
            }
            if (!gpu)
                m_layers[i]->frame(fi).gpu_unload();
        }
    }

    if (ppi_header.doc_version.minor >= 4)
    {
        BinaryStreamWriter sw;
        sw.init(BinaryStream::ByteOrder::LittleEndian);

        Serializer::Descriptor info;
        info.class_id = "ppi_info";
        info.name = L"info header";
        //info.props["has_encoder"] = std::make_shared<Serializer::Boolean>(m_encoder != nullptr);
        sw << info;
        //if (m_encoder != nullptr)
        //    sw << *m_encoder;

        int bytes = sw.m_data.size();
        fwrite(&bytes, sizeof(int), 1, fp);
        fwrite((char*)sw.m_data.data(), sw.m_data.size(), 1, fp);
    }

    fclose(fp);

    bool success = false;
    if (use_tmp)
    {
        LOG("project saved tmp to %s", tmp_path.c_str());
        LOG("swapping to %s", file_path.c_str());
        if (std::remove(file_path.c_str()) == 0)
        {
            if (std::rename(tmp_path.c_str(), file_path.c_str()) == 0)
            {
                success = true;
                LOG("tmp file swapped succesfully");
            }
            else
            {
                success = false;
                LOG("tmp file NOT swapped, original removed");
            }
        }
        else
        {
            success = false;
            LOG("could not remove %s", file_path.c_str());
        }
    }
    else
    {
        success = true;
        LOG("project saved to %s", file_path.c_str());
    }

    if (success)
    {
        m_unsaved = false;
        m_newdoc = false;

        // save timelapse
        if (Canvas::I->m_encoder)
        {
            BinaryStreamWriter sw;
            sw.init(BinaryStream::ByteOrder::LittleEndian);
            Serializer::Descriptor info;
            info.class_id = "tracks-info";
            info.name = L"Timelapse Tracks";
            info.props["has-track-360"] = std::make_shared<Serializer::Boolean>(true);
            info.props["version"] = std::make_shared<Serializer::Integer>(1);
            sw << info;
            sw << *Canvas::I->m_encoder;
            if (!sw.save(lapse_path))
                LOG("cannot save timelase to %s", lapse_path.c_str());
        }
        App::I->flush_platform_storage();
    }

    if (show_progress)
        pb->destroy();
    App::I->title_update();
    
    return success;
}

void Canvas::project_open(std::string file_path, std::function<void(bool)> on_complete)
{
    std::thread t([=] {
        BT_SetTerminate();
        bool result = project_open_thread(file_path);
        if (on_complete)
            on_complete(result);
    });
    t.detach();
}

bool Canvas::project_open_thread(std::string file_path)
{
    FILE* fp = fopen(file_path.c_str(), "rb");
    if (!fp)
    {
        LOG("cannot write project to %s", file_path.c_str());
        return false; // should probably return a bool
    }

    PPIHeader ppi_header;
    fread(&ppi_header, sizeof(PPIHeader), 1, fp);

    if (!ppi_header.valid())
    {
        LOG("INVALID PPI HEADER");
        return false;
    }
    
    std::shared_ptr<NodeProgressBar> pb;
    if (App::I->layout.m_loaded)
    {
        pb = std::make_shared<NodeProgressBar>();
        pb->set_manager(&App::I->layout);
        pb->init();
        pb->create();
        pb->loaded();
        pb->m_progress->SetWidthP(0);
        pb->m_title->set_text("Opening Pano Project");
        App::I->layout[App::I->main_id]->add_child(pb);
    }

    // skip thumbnail
    Image thumb;
    thumb.width = ppi_header.thumb_header.width;
    thumb.height = ppi_header.thumb_header.height;
    thumb.comp = ppi_header.thumb_header.comp;
    fseek(fp, thumb.size(), SEEK_CUR);

    fread(&m_width, sizeof(int), 1, fp);
    fread(&m_height, sizeof(int), 1, fp);
    
    int n_layers = 0;
    fread(&n_layers, sizeof(int), 1, fp);
    int n_frames = 1;
    if (ppi_header.doc_version.minor >= 3)
        fread(&n_frames, sizeof(int), 1, fp);

    const int bytes = m_width * m_height * 4;
    LayerFrame::Snapshot snap;
    snap.create(m_width, m_height); // allocate single data, no box should be bigger

    int progress = 0;
    int total = n_frames * 6;
    
    for (auto& l : m_layers)
        l->destroy();
    m_layers.clear();
    //clear_all();
    resize(m_width, m_height);

    std::vector<std::shared_ptr<Layer>> tmp_layers(n_layers);

    for (int i = 0; i < n_layers; i++)
    {
        int n_order;
        fread(&n_order, sizeof(int), 1, fp);

        //if (ppi_header.doc_version.minor > 1)
        //    n_order = i;

        tmp_layers[n_order] = std::make_unique<Layer>();
        auto& layer = tmp_layers[n_order];

        fread(&layer->m_opacity, sizeof(float), 1, fp);

        int name_len;
        fread(&name_len, sizeof(int), 1, fp);
        std::string name(name_len, '\0');
        fread((char*)name.data(), name_len, 1, fp);

        if (ppi_header.doc_version.minor >= 2)
        {
            fread(&layer->m_blend_mode, sizeof(int), 1, fp);
            fread(&layer->m_alpha_locked, sizeof(bool), 1, fp);
            fread(&layer->m_visible, sizeof(bool), 1, fp);
        }

        int frames = 1;
        if (ppi_header.doc_version.minor >= 3)
            fread(&frames, sizeof(int), 1, fp);

        layer->create(m_width, m_height, name.c_str());

        for (int fi = 0; fi < frames; fi++)
        {
            if (fi > 0)
                layer->add_frame();
            if (ppi_header.doc_version.minor >= 3)
            {
                int duration = layer->frame_duration(fi);
                fread(&duration, sizeof(int), 1, fp);
            }
            snap.clear();
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                int has_data;
                fread(&has_data, sizeof(int), 1, fp);
                snap.m_dirty_face[plane_index] = has_data;
                if (has_data)
                {
                    int b[4];
                    fread(&b, sizeof(b), 1, fp);
                    snap.m_dirty_box[plane_index] = glm::vec4(b[0], b[1], b[2], b[3]);
                    glm::vec2 sz = zw(snap.m_dirty_box[plane_index]) - xy(snap.m_dirty_box[plane_index]);

                    int data_size;
                    fread(&data_size, sizeof(int), 1, fp);
                    std::vector<uint8_t> compressed(data_size);

                    fread(compressed.data(), 1, data_size, fp);
                    int imgw, imgh, imgc;
                    uint8_t* rgba = stbi_load_from_memory(compressed.data(), data_size, &imgw, &imgh, &imgc, 4);
                    if (rgba)
                    {
                        std::copy(rgba, rgba + (imgw * imgh * 4), snap.image[plane_index].get());
                        delete rgba;
                    }
                }

                progress++;
                float p = (float)progress / total * 100.f;
                LOG("progress: %f", p);

                if (App::I->layout.m_loaded)
                {
                    pb->m_progress->SetWidthP(p);
                }
            }
            layer->restore(snap, fi);
        }
    }

    std::swap(tmp_layers, m_layers);

    if (ppi_header.doc_version.minor >= 4)
    {
        int bytes = 0;
        fread(&bytes, sizeof(int), 1, fp);
        std::vector<uint8_t> data(bytes);
        fread(data.data(), bytes, 1, fp);
        BinaryStreamReader sr;
        sr.init(data.data(), data.size(), BinaryStream::ByteOrder::LittleEndian);
        Serializer::Descriptor info;
        sr >> info;
        //if (info.value<Serializer::Boolean>("has_encoder"))
        //{
        //    m_encoder = std::make_unique<MP4Encoder>();
        //    sr >> *m_encoder;
        //    m_encoder->init();
        //}
        //else
        //{
        //    timelapse_reset_encoder();
        //}
    }
    //else
    //{
    //    timelapse_reset_encoder();
    //}

    fclose(fp);
    LOG("project restore from %s", file_path.c_str());

    auto start = file_path.rfind('/') + 1;
    std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi"));
    std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl";
    if (Asset::exist(lapse_path))
    {
        BinaryStreamReader sr;
        sr.load(lapse_path, BinaryStream::ByteOrder::LittleEndian);
        Serializer::Descriptor info;
        sr >> info;
        if (info.value<Serializer::Boolean>("has-track-360"))
        {
            m_encoder = std::make_unique<MP4Encoder>();
            sr >> *m_encoder;
            m_encoder->init();
        }
    }
    else
    {
        timelapse_reset_encoder();
    }
    
    m_current_layer_idx = 0;
    m_current_stroke = nullptr;
    m_dual_stroke = nullptr;
    m_show_tmp = false;
    m_smask_active = false;
    m_smask_mode = 0;
    m_dirty = false;
    m_commit_delayed = false;
    m_dirty_stroke = false;
    memset(m_dirty_face, 0, sizeof(bool) * 6);
    memset(m_pick_ready, 0, sizeof(bool) * 6);
    m_unsaved = false;
    m_newdoc = false;
    if (App::I->layout.m_loaded)
    {
        pb->destroy();
        App::I->ui_task([] {
            App::I->title_update();
            App::I->update_rec_frames();
            Canvas::I->anim_update();
            App::I->animation->load_layers();
        });
    }
    return true;
}

Image Canvas::thumbnail_generate(int w, int h)
{
    Image image;
    image.create(w, h);

    App::I->render_task([this, w, h, &image]
    {
        // save viewport and clear color states
        const auto vp = query_canvas_viewport();
        const auto cc = query_canvas_clear_color();
        auto blend = query_canvas_capability(blend_state());

        // prepare common states
        apply_canvas_viewport(0, 0, w, h);

        RTT fb;
        fb.create(w, h);
        fb.bindFramebuffer();
        Plane m_face_plane;
        m_face_plane.create<1>(2, 2);
        Texture2D blendtex;
        blendtex.create(w, h);
        const auto layer_feedback = canvas_destination_feedback_plan(w, h);
        const bool copy_layer_destination = !layer_feedback.reads_destination_color;

        // recalculate because of different aspect ratio than the m_proj matrix
        glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f);

        fb.clear({ 1, 1, 1, 0 });
        for (int i = 0; i < 6; i++)
        {
            apply_canvas_capability(blend_state(), false);
            auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1));

            ShaderManager::use(kShader::TextureBlend);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
            if (copy_layer_destination)
            {
                ShaderManager::u_int(kShaderUniform::TexBG, 2);
                set_active_texture_unit(2);
                blendtex.bind();
                m_sampler_nearest.bind(2);
            }
            m_sampler_nearest.bind(0);   // nearest
            for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
            {
                if (!m_layers[layer_index]->m_visible ||
                    m_layers[layer_index]->m_opacity == 0.f ||
                    !m_layers[layer_index]->face(i))
                    continue;
                if (copy_layer_destination)
                {
                    set_active_texture_unit(2);
                    copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h);
                }
                ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
                ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(i).bindTexture();
                m_face_plane.draw_fill();
                m_layers[layer_index]->rtt(i).unbindTexture();
            }

            if (copy_layer_destination)
            {
                set_active_texture_unit(2);
                blendtex.unbind();
            }

            set_active_texture_unit(0);
            blendtex.bind();
            // copy the content of the fb before drawing the grid
            copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h);

            // draw the grid
            ShaderManager::use(kShader::Checkerboard);
            ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
            m_face_plane.draw_fill();

            // now blend with the background
            apply_canvas_capability(blend_state(), true);
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
            m_sampler.bind(0); // linear
            m_plane.draw_fill();

            blendtex.unbind();
        }

        fb.unbindFramebuffer();

        // read the rendered image
        fb.readTextureData((uint8_t*)image.data());

        fb.destroy();
        blendtex.destroy();

        // restore viewport and clear color states
        blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
        apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
        apply_canvas_clear_color(cc);
        set_active_texture_unit(0);
    });

    return image;
}

Image Canvas::thumbnail_read(std::string file_path)
{
//     static char name[128];
//     sprintf(name, "%s/latlong.ppi", data_path.c_str());
    FILE* fp = fopen(file_path.c_str(), "rb");
    if (!fp)
    {
        LOG("cannot read project %s", file_path.c_str());
        return {}; // return empty image
    }
    PPIHeader ppi_header;
    fread(&ppi_header, sizeof(PPIHeader), 1, fp);

    if (!ppi_header.valid())
        return {};

    Image thumb;
    thumb.width = ppi_header.thumb_header.width;
    thumb.height = ppi_header.thumb_header.height;
    thumb.comp = ppi_header.thumb_header.comp;
    thumb.create();
    fread((uint8_t*)thumb.data(), thumb.size(), 1, fp);
    fclose(fp);
    LOG("project thumbnail read from %s", file_path.c_str());
    return thumb;
}

void Canvas::draw_objects_direct(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, Layer& layer, int frame)
{
    App::I->render_task([&]
    {
        // save viewport and clear color states
        const auto vp = query_canvas_viewport();
        const auto cc = query_canvas_clear_color();
        auto blend = query_canvas_capability(blend_state());

        // prepare common states
        apply_canvas_viewport(0, 0, layer.w, layer.h);
        apply_canvas_capability(blend_state(), false);

        GLuint rboID = allocate_canvas_depth_renderbuffer(layer.w, layer.h);

        glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f);
        for (int i = 0; i < 6; i++)
        {
            glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]);
            layer.rtt(i, frame).bindFramebuffer();
            attach_canvas_depth_renderbuffer(rboID);

            observer(plane_camera, proj, i);

            attach_canvas_depth_renderbuffer(0);
            layer.rtt(i, frame).unbindFramebuffer();

            layer.face(i, frame) = true;
            layer.box(i, frame) = { 0, 0, layer.w, layer.h };
        }

        delete_canvas_renderbuffer(rboID);

        // restore viewport and clear color states
        blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
        apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
        apply_canvas_clear_color(cc);
        set_active_texture_unit(0);
    });
}

void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, Layer& layer, int frame, bool save_history)
{
    App::I->render_task([&]
    {
        // save viewport and clear color states
        const auto vp = query_canvas_viewport();
        const auto cc = query_canvas_clear_color();
        auto blend = query_canvas_capability(blend_state());

        // prepare common states
        apply_canvas_viewport(0, 0, layer.w, layer.h);
        apply_canvas_capability(blend_state(), false);

        GLuint rboID = allocate_canvas_depth_renderbuffer(layer.w, layer.h);

        RTT rtt;
        rtt.create(layer.w, layer.h);
        rtt.bindFramebuffer();
        attach_canvas_depth_renderbuffer(rboID);
        rtt.unbindFramebuffer();

        // allocate action to add to history
        ActionStroke* action;
        
        if (save_history)
        {
            action = new ActionStroke;
            action->was_saved = !m_unsaved;
        }

        glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f);
        for (int i = 0; i < 6; i++)
        {
            glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]);
            rtt.bindFramebuffer();
            rtt.clear({ 1, 1, 1, 0 });
            observer(plane_camera, proj, i);
            rtt.unbindFramebuffer();

            glm::vec4 bounds = rtt.calc_bounds();

            layer.rtt(i, frame).bindFramebuffer();

            glm::vec2 box_sz = zw(bounds) - xy(bounds);
            bool has_data = box_sz.x > 0 && box_sz.y > 0;

            if (save_history)
            {
                // save image before commit
                if (has_data)
                {
                    action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
                    layer.rtt(i, frame).readPixelsRgba8(
                        static_cast<int>(bounds.x),
                        static_cast<int>(bounds.y),
                        static_cast<int>(box_sz.x),
                        static_cast<int>(box_sz.y),
                        action->m_image[i].get());
                    action->m_box[i] = bounds;
                }
                action->m_old_box[i] = layer.box(i, frame);
                action->m_old_dirty[i] = layer.face(i, frame);
            }

            // draw the tmp layer into the actual layer
            if (has_data)
            {
                ShaderManager::use(kShader::Texture);
                ShaderManager::u_int(kShaderUniform::Tex, 0);
                ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f));
                set_active_texture_unit(0);
                m_sampler_nearest.bind(0);
                rtt.bindTexture();
                m_plane.draw_fill();
                rtt.unbindTexture();

                layer.face(i, frame) = true;
                layer.box(i, frame) = { glm::min(xy(layer.box(i, frame)), xy(bounds)), glm::max(zw(layer.box(i, frame)), zw(bounds)) };
            }

            layer.rtt(i, frame).unbindFramebuffer();
        }

        if (save_history)
        {
            // save history
            action->m_layer_idx = m_current_layer_idx;
            action->m_frame_idx = frame;
            action->m_canvas = this;
            //action->m_stroke = std::move(m_current_stroke);
            ActionManager::add(action);
        }

        delete_canvas_renderbuffer(rboID);
        rtt.destroy();

        // restore viewport and clear color states
        blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
        apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
        apply_canvas_clear_color(cc);
        set_active_texture_unit(0);
    });
}

void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, int frame, bool save_history)
{
    draw_objects(observer, layer(), frame, save_history);
}

void Canvas::project2Dpoints(std::vector<vertex_t>& vertices)
{
    for (auto& p : vertices)
    {
        glm::vec3 ro, rd, hit_o, hit_d;
        glm::vec2 hit_fb;
        int plane_id;
        if (point_trace(p.pos, ro, rd, hit_o, hit_fb, hit_d, plane_id))
            p.pos = glm::vec4(hit_o, 1);
    }
}

glm::vec3 Canvas::project2Dpoint(glm::vec2 pt)
{
    glm::vec3 ro, rd, hit_o, hit_d;
    glm::vec2 hit_fb;
    int plane_id;
    if (point_trace(pt, ro, rd, hit_o, hit_fb, hit_d, plane_id))
        return glm::vec4(hit_o, 1);
    return glm::vec3(0);
}



// return the 2d shape of the faces based on the current camera
// this can be used for screen space shapes clipping
std::vector<glm::vec2> Canvas::face_to_shape2D(int plane_index)
{
    static std::array<glm::vec4, 4> corners{
        glm::vec4(-1.f, +1.f, -1.f, 1.f), // A top-left
        glm::vec4(+1.f, +1.f, -1.f, 1.f), // B top-right
        glm::vec4(+1.f, -1.f, -1.f, 1.f), // C bottom-right
        glm::vec4(-1.f, -1.f, -1.f, 1.f), // D bottom-left
    };

    // compute points in camera space
    std::vector<glm::vec3> pt_cam;
    for (auto c : corners)
    {
        auto pt_world = m_plane_transform[plane_index] * c;
        pt_cam.push_back(m_mv * pt_world);
    }

    // clip at near plane
    pt_cam = poly_clip_near(pt_cam, 0.01);

    // compute windows space
    std::vector<glm::vec2> points;
    for (auto p : pt_cam)
    {
        auto pt_clip = m_proj * glm::vec4(p, 1);
        pt_clip = pt_clip / pt_clip.w;
        glm::vec2 pt_screen = (glm::vec2(pt_clip) * 0.5f + 0.5f) * zw(m_vp);
        pt_screen.y = m_vp.w - pt_screen.y - 1;
        points.push_back(pt_screen);
    }
    return points;
}

void Canvas::push_camera()
{
    m_camera_stack.push(get_camera());
}

void Canvas::pop_camera()
{
    if (!m_camera_stack.empty())
    {
        set_camera(m_camera_stack.top());
        m_camera_stack.pop();
    }
}

CameraData Canvas::get_camera()
{
    CameraData c;
    c.m_box = m_box;
    c.m_mv = m_mv;
    c.m_pan = m_pan;
    std::copy_n(m_plane_dir, 6, c.m_plane_dir);
    std::copy_n(m_plane_unproject, 6, c.m_plane_unproject);
    c.m_proj = m_proj;
    c.m_vp = m_vp;
    return c;
}

void Canvas::set_camera(const CameraData& c)
{
    m_box = c.m_box;
    m_mv = c.m_mv;
    m_pan = c.m_pan;
    std::copy_n(c.m_plane_dir, 6, m_plane_dir);
    std::copy_n(c.m_plane_unproject, 6, m_plane_unproject);
    m_proj = c.m_proj;
    m_vp = c.m_vp;
}

void Canvas::timelapse_reset_encoder() noexcept
{
    m_encoder = std::make_unique<MP4Encoder>();
    int res = glm::min<int>(m_width / 2, 1024);
    m_encoder->init(res * 4, res * 2, 30, 2000 << 10);
}
