#include <GL/glew.h>

#include "3DScene.hpp"

#include "../../libslic3r/libslic3r.h"
#include "../../libslic3r/ExtrusionEntity.hpp"
#include "../../libslic3r/ExtrusionEntityCollection.hpp"
#include "../../libslic3r/Geometry.hpp"
#include "../../libslic3r/GCode/PreviewData.hpp"
#include "../../libslic3r/Print.hpp"
#include "../../libslic3r/Slicing.hpp"
#include "../../slic3r/GUI/PresetBundle.hpp"
#include "GCode/Analyzer.hpp"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <utility>
#include <assert.h>

#include <boost/log/trivial.hpp>

#include <tbb/parallel_for.h>
#include <tbb/spin_mutex.h>

#include <wx/bitmap.h>
#include <wx/dcmemory.h>
#include <wx/image.h>
#include <wx/settings.h>

#include "GUI.hpp"

namespace Slic3r {

void GLIndexedVertexArray::load_mesh_flat_shading(const TriangleMesh &mesh)
{
    assert(triangle_indices.empty() && vertices_and_normals_interleaved_size == 0);
    assert(quad_indices.empty() && triangle_indices_size == 0);
    assert(vertices_and_normals_interleaved.size() % 6 == 0 && quad_indices_size == vertices_and_normals_interleaved.size());

    this->vertices_and_normals_interleaved.reserve(this->vertices_and_normals_interleaved.size() + 3 * 3 * 2 * mesh.facets_count());
    
    for (int i = 0; i < mesh.stl.stats.number_of_facets; ++ i) {
        const stl_facet &facet = mesh.stl.facet_start[i];
        for (int j = 0; j < 3; ++ j)
            this->push_geometry(facet.vertex[j].x, facet.vertex[j].y, facet.vertex[j].z, facet.normal.x, facet.normal.y, facet.normal.z);
    }
}

void GLIndexedVertexArray::load_mesh_full_shading(const TriangleMesh &mesh)
{
    assert(triangle_indices.empty() && vertices_and_normals_interleaved_size == 0);
    assert(quad_indices.empty() && triangle_indices_size == 0);
    assert(vertices_and_normals_interleaved.size() % 6 == 0 && quad_indices_size == vertices_and_normals_interleaved.size());

    this->vertices_and_normals_interleaved.reserve(this->vertices_and_normals_interleaved.size() + 3 * 3 * 2 * mesh.facets_count());

    unsigned int vertices_count = 0;
    for (int i = 0; i < mesh.stl.stats.number_of_facets; ++i) {
        const stl_facet &facet = mesh.stl.facet_start[i];
        for (int j = 0; j < 3; ++j)
            this->push_geometry(facet.vertex[j].x, facet.vertex[j].y, facet.vertex[j].z, facet.normal.x, facet.normal.y, facet.normal.z);

        this->push_triangle(vertices_count, vertices_count + 1, vertices_count + 2);
        vertices_count += 3;
    }
}

void GLIndexedVertexArray::finalize_geometry(bool use_VBOs)
{
    assert(this->vertices_and_normals_interleaved_VBO_id == 0);
    assert(this->triangle_indices_VBO_id == 0);
    assert(this->quad_indices_VBO_id == 0);

    this->setup_sizes();

    if (use_VBOs) {
        if (! empty()) {
            glGenBuffers(1, &this->vertices_and_normals_interleaved_VBO_id);
            glBindBuffer(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved_VBO_id);
            glBufferData(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved.size() * 4, this->vertices_and_normals_interleaved.data(), GL_STATIC_DRAW);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            this->vertices_and_normals_interleaved.clear();
        }
        if (! this->triangle_indices.empty()) {
            glGenBuffers(1, &this->triangle_indices_VBO_id);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices_VBO_id);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices.size() * 4, this->triangle_indices.data(), GL_STATIC_DRAW);
            this->triangle_indices.clear();
        }
        if (! this->quad_indices.empty()) {
            glGenBuffers(1, &this->quad_indices_VBO_id);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices_VBO_id);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices.size() * 4, this->quad_indices.data(), GL_STATIC_DRAW);
            this->quad_indices.clear();
        }
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    this->shrink_to_fit();
}

void GLIndexedVertexArray::release_geometry()
{
    if (this->vertices_and_normals_interleaved_VBO_id)
        glDeleteBuffers(1, &this->vertices_and_normals_interleaved_VBO_id);
    if (this->triangle_indices_VBO_id)
        glDeleteBuffers(1, &this->triangle_indices_VBO_id);
    if (this->quad_indices_VBO_id)
        glDeleteBuffers(1, &this->quad_indices_VBO_id);
    this->clear();
    this->shrink_to_fit();
}

void GLIndexedVertexArray::render() const
{
    if (this->vertices_and_normals_interleaved_VBO_id) {
        glBindBuffer(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved_VBO_id);
        glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)));
        glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr);
    } else {
        glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data() + 3);
        glNormalPointer(GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data());
    }
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);

    if (this->indexed()) {
        if (this->vertices_and_normals_interleaved_VBO_id) {
            // Render using the Vertex Buffer Objects.
            if (this->triangle_indices_size > 0) {
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices_VBO_id);
                glDrawElements(GL_TRIANGLES, GLsizei(this->triangle_indices_size), GL_UNSIGNED_INT, nullptr);
            }
            if (this->quad_indices_size > 0) {
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices_VBO_id);
                glDrawElements(GL_QUADS, GLsizei(this->quad_indices_size), GL_UNSIGNED_INT, nullptr);
            }
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        } else {
            // Render in an immediate mode.
            if (! this->triangle_indices.empty())
                glDrawElements(GL_TRIANGLES, GLsizei(this->triangle_indices_size), GL_UNSIGNED_INT, this->triangle_indices.data());
            if (! this->quad_indices.empty())
                glDrawElements(GL_QUADS, GLsizei(this->quad_indices_size), GL_UNSIGNED_INT, this->quad_indices.data());
        }
    } else
        glDrawArrays(GL_TRIANGLES, 0, GLsizei(this->vertices_and_normals_interleaved_size / 6));

    if (this->vertices_and_normals_interleaved_VBO_id)
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
}

void GLIndexedVertexArray::render(
    const std::pair<size_t, size_t> &tverts_range,
    const std::pair<size_t, size_t> &qverts_range) const 
{
    assert(this->indexed());
    if (! this->indexed())
        return;

    if (this->vertices_and_normals_interleaved_VBO_id) {
        // Render using the Vertex Buffer Objects.
        glBindBuffer(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved_VBO_id);
        glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)));
        glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr);
        glEnableClientState(GL_VERTEX_ARRAY);
        glEnableClientState(GL_NORMAL_ARRAY);
        if (this->triangle_indices_size > 0) {
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices_VBO_id);
            glDrawElements(GL_TRIANGLES, GLsizei(std::min(this->triangle_indices_size, tverts_range.second - tverts_range.first)), GL_UNSIGNED_INT, (const void*)(tverts_range.first * 4));
        }
        if (this->quad_indices_size > 0) {
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices_VBO_id);
            glDrawElements(GL_QUADS, GLsizei(std::min(this->quad_indices_size, qverts_range.second - qverts_range.first)), GL_UNSIGNED_INT, (const void*)(qverts_range.first * 4));
        }
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    } else {
        // Render in an immediate mode.
        glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data() + 3);
        glNormalPointer(GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data());
        glEnableClientState(GL_VERTEX_ARRAY);
        glEnableClientState(GL_NORMAL_ARRAY);
        if (! this->triangle_indices.empty())
            glDrawElements(GL_TRIANGLES, GLsizei(std::min(this->triangle_indices_size, tverts_range.second - tverts_range.first)), GL_UNSIGNED_INT, (const void*)(this->triangle_indices.data() + tverts_range.first));
        if (! this->quad_indices.empty())
            glDrawElements(GL_QUADS, GLsizei(std::min(this->quad_indices_size, qverts_range.second - qverts_range.first)), GL_UNSIGNED_INT, (const void*)(this->quad_indices.data() + qverts_range.first));
    }

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
}

const float GLVolume::SELECTED_COLOR[4] = { 0.0f, 1.0f, 0.0f, 1.0f };
const float GLVolume::HOVER_COLOR[4] = { 0.4f, 0.9f, 0.1f, 1.0f };
const float GLVolume::OUTSIDE_COLOR[4] = { 0.0f, 0.38f, 0.8f, 1.0f };
const float GLVolume::SELECTED_OUTSIDE_COLOR[4] = { 0.19f, 0.58f, 1.0f, 1.0f };

void GLVolume::set_render_color(float r, float g, float b, float a)
{
    render_color[0] = r;
    render_color[1] = g;
    render_color[2] = b;
    render_color[3] = a;
}

void GLVolume::set_render_color(const float* rgba, unsigned int size)
{
    size = std::min((unsigned int)4, size);
    for (int i = 0; i < size; ++i)
    {
        render_color[i] = rgba[i];
    }
}

void GLVolume::set_render_color()
{
    if (selected)
    {
        if (is_outside)
            set_render_color(SELECTED_OUTSIDE_COLOR, 4);
        else
            set_render_color(SELECTED_COLOR, 4);
    }
    else if (hover)
        set_render_color(HOVER_COLOR, 4);
    else if (is_outside)
        set_render_color(OUTSIDE_COLOR, 4);
    else
        set_render_color(color, 4);
}

void GLVolume::set_range(double min_z, double max_z)
{
    this->qverts_range.first  = 0;
    this->qverts_range.second = this->indexed_vertex_array.quad_indices_size;
    this->tverts_range.first  = 0;
    this->tverts_range.second = this->indexed_vertex_array.triangle_indices_size;
    if (! this->print_zs.empty()) {
        // The Z layer range is specified.
        // First test whether the Z span of this object is not out of (min_z, max_z) completely.
        if (this->print_zs.front() > max_z || this->print_zs.back() < min_z) {
            this->qverts_range.second = 0;
            this->tverts_range.second = 0;
        } else {
            // Then find the lowest layer to be displayed.
            size_t i = 0;
            for (; i < this->print_zs.size() && this->print_zs[i] < min_z; ++ i);
            if (i == this->print_zs.size()) {
                // This shall not happen.
                this->qverts_range.second = 0;
                this->tverts_range.second = 0;
            } else {
                // Remember start of the layer.
                this->qverts_range.first = this->offsets[i * 2];
                this->tverts_range.first = this->offsets[i * 2 + 1];
                // Some layers are above $min_z. Which?
                for (; i < this->print_zs.size() && this->print_zs[i] <= max_z; ++ i);
                if (i < this->print_zs.size()) {
                    this->qverts_range.second = this->offsets[i * 2];
                    this->tverts_range.second = this->offsets[i * 2 + 1];
                }
            }
        }
    }
}

void GLVolume::render() const
{
    if (!is_active)
        return;

    glCullFace(GL_BACK);
    glPushMatrix();
    glTranslated(this->origin.x, this->origin.y, this->origin.z);
    if (this->indexed_vertex_array.indexed())
        this->indexed_vertex_array.render(this->tverts_range, this->qverts_range);
    else
        this->indexed_vertex_array.render();
    glPopMatrix();
}

void GLVolume::render_using_layer_height() const
{
    if (!is_active)
        return;

    GLint current_program_id;
    glGetIntegerv(GL_CURRENT_PROGRAM, &current_program_id);

    if ((layer_height_texture_data.shader_id > 0) && (layer_height_texture_data.shader_id != current_program_id))
        glUseProgram(layer_height_texture_data.shader_id);

    GLint z_to_texture_row_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_to_texture_row") : -1;
    GLint z_texture_row_to_normalized_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_texture_row_to_normalized") : -1;
    GLint z_cursor_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_cursor") : -1;
    GLint z_cursor_band_width_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_cursor_band_width") : -1;

    if (z_to_texture_row_id  >= 0)
        glUniform1f(z_to_texture_row_id, (GLfloat)layer_height_texture_z_to_row_id());

    if (z_texture_row_to_normalized_id >= 0)
        glUniform1f(z_texture_row_to_normalized_id, (GLfloat)(1.0f / layer_height_texture_height()));

    if (z_cursor_id >= 0)
        glUniform1f(z_cursor_id, (GLfloat)(layer_height_texture_data.print_object->model_object()->bounding_box().max.z * layer_height_texture_data.z_cursor_relative));

    if (z_cursor_band_width_id >= 0)
        glUniform1f(z_cursor_band_width_id, (GLfloat)layer_height_texture_data.edit_band_width);

    unsigned int w = layer_height_texture_width();
    unsigned int h = layer_height_texture_height();

    glBindTexture(GL_TEXTURE_2D, layer_height_texture_data.texture_id);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, w / 2, h / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, layer_height_texture_data_ptr_level0());
    glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, w / 2, h / 2, GL_RGBA, GL_UNSIGNED_BYTE, layer_height_texture_data_ptr_level1());

    render();

    glBindTexture(GL_TEXTURE_2D, 0);

    if ((current_program_id > 0) && (layer_height_texture_data.shader_id != current_program_id))
        glUseProgram(current_program_id);
}

double GLVolume::layer_height_texture_z_to_row_id() const
{
    return (this->layer_height_texture.get() == nullptr) ? 0.0 : double(this->layer_height_texture->cells - 1) / (double(this->layer_height_texture->width) * this->layer_height_texture_data.print_object->model_object()->bounding_box().max.z);
}

void GLVolume::generate_layer_height_texture(PrintObject *print_object, bool force)
{
    GLTexture *tex = this->layer_height_texture.get();
	if (tex == nullptr)
		// No layer_height_texture is assigned to this GLVolume, therefore the layer height texture cannot be filled.
		return;

	// Always try to update the layer height profile.
	bool update = print_object->update_layer_height_profile(print_object->model_object()->layer_height_profile) || force;
	// Update if the layer height profile was changed, or when the texture is not valid.
	if (! update && ! tex->data.empty() && tex->cells > 0)
        // Texture is valid, don't update.
        return; 

    if (tex->data.empty()) {
        tex->width  = 1024;
        tex->height = 1024;
        tex->levels = 2;
        tex->data.assign(tex->width * tex->height * 5, 0);
    }

    SlicingParameters slicing_params = print_object->slicing_parameters();
    bool level_of_detail_2nd_level = true;
    tex->cells = Slic3r::generate_layer_height_texture(
        slicing_params, 
        Slic3r::generate_object_layers(slicing_params, print_object->model_object()->layer_height_profile), 
        tex->data.data(), tex->height, tex->width, level_of_detail_2nd_level);
}

// 512x512 bitmaps are supported everywhere, but that may not be sufficent for super large print volumes.
#define LAYER_HEIGHT_TEXTURE_WIDTH  1024
#define LAYER_HEIGHT_TEXTURE_HEIGHT 1024

std::vector<int> GLVolumeCollection::load_object(
    const ModelObject       *model_object, 
    int                      obj_idx,
    const std::vector<int>  &instance_idxs,
    const std::string       &color_by,
    const std::string       &select_by,
    const std::string       &drag_by,
    bool                     use_VBOs)
{
    static float colors[4][4] = {
        { 1.0f, 1.0f, 0.0f, 1.f }, 
        { 1.0f, 0.5f, 0.5f, 1.f },
        { 0.5f, 1.0f, 0.5f, 1.f }, 
        { 0.5f, 0.5f, 1.0f, 1.f }
    };

    // Object will have a single common layer height texture for all volumes.
    std::shared_ptr<GLTexture> layer_height_texture = std::make_shared<GLTexture>();
    
    std::vector<int> volumes_idx;
    for (int volume_idx = 0; volume_idx < int(model_object->volumes.size()); ++ volume_idx) {
        const ModelVolume *model_volume = model_object->volumes[volume_idx];

        int extruder_id = -1;
        if (!model_volume->modifier)
        {
            extruder_id = model_volume->config.has("extruder") ? model_volume->config.option("extruder")->getInt() : 0;
            if (extruder_id == 0)
                extruder_id = model_object->config.has("extruder") ? model_object->config.option("extruder")->getInt() : 0;
        }

        for (int instance_idx : instance_idxs) {
            const ModelInstance *instance = model_object->instances[instance_idx];
            TriangleMesh mesh = model_volume->mesh;
            instance->transform_mesh(&mesh);
            volumes_idx.push_back(int(this->volumes.size()));
            float color[4];
            memcpy(color, colors[((color_by == "volume") ? volume_idx : obj_idx) % 4], sizeof(float) * 3);
            color[3] = model_volume->modifier ? 0.5f : 1.f;
            this->volumes.emplace_back(new GLVolume(color));
            GLVolume &v = *this->volumes.back();
            if (use_VBOs)
                v.indexed_vertex_array.load_mesh_full_shading(mesh);
            else
                v.indexed_vertex_array.load_mesh_flat_shading(mesh);

            // finalize_geometry() clears the vertex arrays, therefore the bounding box has to be computed before finalize_geometry().
            v.bounding_box = v.indexed_vertex_array.bounding_box();
            v.indexed_vertex_array.finalize_geometry(use_VBOs);
            v.composite_id = obj_idx * 1000000 + volume_idx * 1000 + instance_idx;
            if (select_by == "object")
                v.select_group_id = obj_idx * 1000000;
            else if (select_by == "volume")
                v.select_group_id = obj_idx * 1000000 + volume_idx * 1000;
            else if (select_by == "instance")
                v.select_group_id = v.composite_id;
            if (drag_by == "object")
                v.drag_group_id = obj_idx * 1000;
            else if (drag_by == "instance")
                v.drag_group_id = obj_idx * 1000 + instance_idx;

            if (!model_volume->modifier)
            {
                v.layer_height_texture = layer_height_texture;
                if (extruder_id != -1)
                    v.extruder_id = extruder_id;
            }
            v.is_modifier = model_volume->modifier;
            v.outside_printer_detection_enabled = !model_volume->modifier;
        }
    }
    
    return volumes_idx; 
}


int GLVolumeCollection::load_wipe_tower_preview(
    int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool use_VBOs)
{
    float color[4] = { 0.5f, 0.5f, 0.0f, 0.5f };
    this->volumes.emplace_back(new GLVolume(color));
    GLVolume &v = *this->volumes.back();

    if (height == 0.0f)
        height = 0.1f;

    auto mesh = make_cube(width, depth, height);
    mesh.translate(-width / 2.f, -depth / 2.f, 0.f);
    Point origin_of_rotation(0.f, 0.f);
    mesh.rotate(rotation_angle,&origin_of_rotation);

    if (use_VBOs)
        v.indexed_vertex_array.load_mesh_full_shading(mesh);
    else
        v.indexed_vertex_array.load_mesh_flat_shading(mesh);

    v.origin = Pointf3(pos_x, pos_y, 0.);
    // finalize_geometry() clears the vertex arrays, therefore the bounding box has to be computed before finalize_geometry().
    v.bounding_box = v.indexed_vertex_array.bounding_box();
    v.indexed_vertex_array.finalize_geometry(use_VBOs);
    v.composite_id = obj_idx * 1000000;
    v.select_group_id = obj_idx * 1000000;
    v.drag_group_id = obj_idx * 1000;
    v.is_wipe_tower = true;
    return int(this->volumes.size() - 1);
}

void GLVolumeCollection::render_VBOs() const
{
    ::glEnable(GL_BLEND);
    ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    ::glCullFace(GL_BACK);
    ::glEnableClientState(GL_VERTEX_ARRAY);
    ::glEnableClientState(GL_NORMAL_ARRAY);
 
    GLint current_program_id;
    ::glGetIntegerv(GL_CURRENT_PROGRAM, &current_program_id);
    GLint color_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "uniform_color") : -1;
    GLint print_box_min_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.min") : -1;
    GLint print_box_max_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.max") : -1;
    GLint print_box_origin_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.volume_origin") : -1;

    for (GLVolume *volume : this->volumes) {
        if (!volume->is_active)
            continue;

        if (!volume->indexed_vertex_array.vertices_and_normals_interleaved_VBO_id)
            continue;

        if (volume->layer_height_texture_data.can_use())
        {
            ::glDisableClientState(GL_VERTEX_ARRAY);
            ::glDisableClientState(GL_NORMAL_ARRAY);
            volume->generate_layer_height_texture(volume->layer_height_texture_data.print_object, false);
            volume->render_using_layer_height();
            ::glEnableClientState(GL_VERTEX_ARRAY);
            ::glEnableClientState(GL_NORMAL_ARRAY);
            continue;
        }

        volume->set_render_color();

        GLsizei n_triangles = GLsizei(std::min(volume->indexed_vertex_array.triangle_indices_size, volume->tverts_range.second - volume->tverts_range.first));
        GLsizei n_quads     = GLsizei(std::min(volume->indexed_vertex_array.quad_indices_size,     volume->qverts_range.second - volume->qverts_range.first));
        if (n_triangles + n_quads == 0)
        {
            ::glDisableClientState(GL_VERTEX_ARRAY);
            ::glDisableClientState(GL_NORMAL_ARRAY);

            if (color_id >= 0)
            {
                float color[4];
                ::memcpy((void*)color, (const void*)volume->render_color, 4 * sizeof(float));
                ::glUniform4fv(color_id, 1, (const GLfloat*)color);
            }
            else
                ::glColor4f(volume->render_color[0], volume->render_color[1], volume->render_color[2], volume->render_color[3]);

            if (print_box_min_id != -1)
                ::glUniform3fv(print_box_min_id, 1, (const GLfloat*)print_box_min);

            if (print_box_max_id != -1)
                ::glUniform3fv(print_box_max_id, 1, (const GLfloat*)print_box_max);

            if (print_box_origin_id != -1)
            {
                float origin[4] = { (float)volume->origin.x, (float)volume->origin.y, (float)volume->origin.z, volume->outside_printer_detection_enabled ? 1.0f : 0.0f };
                ::glUniform4fv(print_box_origin_id, 1, (const GLfloat*)origin);
            }

            volume->render();

            ::glEnableClientState(GL_VERTEX_ARRAY);
            ::glEnableClientState(GL_NORMAL_ARRAY);

            continue;
        }

        if (color_id >= 0)
            ::glUniform4fv(color_id, 1, (const GLfloat*)volume->render_color);
        else
            ::glColor4f(volume->render_color[0], volume->render_color[1], volume->render_color[2], volume->render_color[3]);

        if (print_box_min_id != -1)
            ::glUniform3fv(print_box_min_id, 1, (const GLfloat*)print_box_min);

        if (print_box_max_id != -1)
            ::glUniform3fv(print_box_max_id, 1, (const GLfloat*)print_box_max);

        if (print_box_origin_id != -1)
        {
            float origin[4] = { (float)volume->origin.x, (float)volume->origin.y, (float)volume->origin.z, volume->outside_printer_detection_enabled ? 1.0f : 0.0f };
            ::glUniform4fv(print_box_origin_id, 1, (const GLfloat*)origin);
        }

        ::glBindBuffer(GL_ARRAY_BUFFER, volume->indexed_vertex_array.vertices_and_normals_interleaved_VBO_id);
        ::glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)));
        ::glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr);

        bool has_offset = (volume->origin.x != 0) || (volume->origin.y != 0) || (volume->origin.z != 0);
        if (has_offset) {
            ::glPushMatrix();
            ::glTranslated(volume->origin.x, volume->origin.y, volume->origin.z);
        }

        if (n_triangles > 0) {
            ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, volume->indexed_vertex_array.triangle_indices_VBO_id);
            ::glDrawElements(GL_TRIANGLES, n_triangles, GL_UNSIGNED_INT, (const void*)(volume->tverts_range.first * 4));
        }
        if (n_quads > 0) {
            ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, volume->indexed_vertex_array.quad_indices_VBO_id);
            ::glDrawElements(GL_QUADS, n_quads, GL_UNSIGNED_INT, (const void*)(volume->qverts_range.first * 4));
        }

        if (has_offset)
            ::glPopMatrix();
    }

    ::glBindBuffer(GL_ARRAY_BUFFER, 0);
    ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    ::glDisableClientState(GL_VERTEX_ARRAY);
    ::glDisableClientState(GL_NORMAL_ARRAY);

    ::glDisable(GL_BLEND);
}

void GLVolumeCollection::render_legacy() const
{
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glCullFace(GL_BACK);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
 
    for (GLVolume *volume : this->volumes) {
        assert(! volume->indexed_vertex_array.vertices_and_normals_interleaved_VBO_id);
        if (!volume->is_active)
            continue;

        volume->set_render_color();

        GLsizei n_triangles = GLsizei(std::min(volume->indexed_vertex_array.triangle_indices_size, volume->tverts_range.second - volume->tverts_range.first));
        GLsizei n_quads     = GLsizei(std::min(volume->indexed_vertex_array.quad_indices_size,     volume->qverts_range.second - volume->qverts_range.first));
        if (n_triangles + n_quads == 0)
        {
            ::glDisableClientState(GL_VERTEX_ARRAY);
            ::glDisableClientState(GL_NORMAL_ARRAY);

            ::glColor4f(volume->render_color[0], volume->render_color[1], volume->render_color[2], volume->render_color[3]);
            volume->render();

            ::glEnableClientState(GL_VERTEX_ARRAY);
            ::glEnableClientState(GL_NORMAL_ARRAY);

            continue;
        }

        glColor4f(volume->render_color[0], volume->render_color[1], volume->render_color[2], volume->render_color[3]);
        glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), volume->indexed_vertex_array.vertices_and_normals_interleaved.data() + 3);
        glNormalPointer(GL_FLOAT, 6 * sizeof(float), volume->indexed_vertex_array.vertices_and_normals_interleaved.data());
        bool has_offset = volume->origin.x != 0 || volume->origin.y != 0 || volume->origin.z != 0;
        if (has_offset) {
            glPushMatrix();
            glTranslated(volume->origin.x, volume->origin.y, volume->origin.z);
        }
        if (n_triangles > 0)
            glDrawElements(GL_TRIANGLES, n_triangles, GL_UNSIGNED_INT, volume->indexed_vertex_array.triangle_indices.data() + volume->tverts_range.first);
        if (n_quads > 0)
            glDrawElements(GL_QUADS, n_quads, GL_UNSIGNED_INT, volume->indexed_vertex_array.quad_indices.data() + volume->qverts_range.first);
        if (has_offset)
            glPopMatrix();
    }

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);

    glDisable(GL_BLEND);
}

bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config)
{
    if (config == nullptr)
        return false;

    const ConfigOptionPoints* opt = dynamic_cast<const ConfigOptionPoints*>(config->option("bed_shape"));
    if (opt == nullptr)
        return false;

    BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values));
    BoundingBoxf3 print_volume(Pointf3(unscale(bed_box_2D.min.x), unscale(bed_box_2D.min.y), 0.0), Pointf3(unscale(bed_box_2D.max.x), unscale(bed_box_2D.max.y), config->opt_float("max_print_height")));
    // Allow the objects to protrude below the print bed
    print_volume.min.z = -1e10;

    bool contained = true;
    for (GLVolume* volume : this->volumes)
    {
        if ((volume != nullptr) && !volume->is_modifier)
        {
            bool state = print_volume.contains(volume->transformed_bounding_box());
            contained &= state;
            volume->is_outside = !state;
        }
    }

    return contained;
}

void GLVolumeCollection::reset_outside_state()
{
    for (GLVolume* volume : this->volumes)
    {
        if (volume != nullptr)
            volume->is_outside = false;
    }
}

void GLVolumeCollection::update_colors_by_extruder(const DynamicPrintConfig* config)
{
    static const float inv_255 = 1.0f / 255.0f;

    struct Color
    {
        std::string text;
        unsigned char rgb[3];

        Color()
            : text("")
        {
            rgb[0] = 255;
            rgb[1] = 255;
            rgb[2] = 255;
        }

        void set(const std::string& text, unsigned char* rgb)
        {
            this->text = text;
            ::memcpy((void*)this->rgb, (const void*)rgb, 3 * sizeof(unsigned char));
        }
    };

    if (config == nullptr)
        return;

    const ConfigOptionStrings* extruders_opt = dynamic_cast<const ConfigOptionStrings*>(config->option("extruder_colour"));
    if (extruders_opt == nullptr)
        return;

    const ConfigOptionStrings* filamemts_opt = dynamic_cast<const ConfigOptionStrings*>(config->option("filament_colour"));
    if (filamemts_opt == nullptr)
        return;

    unsigned int colors_count = std::max((unsigned int)extruders_opt->values.size(), (unsigned int)filamemts_opt->values.size());
    if (colors_count == 0)
        return;

    std::vector<Color> colors(colors_count);

    unsigned char rgb[3];
    for (unsigned int i = 0; i < colors_count; ++i)
    {
        const std::string& txt_color = config->opt_string("extruder_colour", i);
        if (PresetBundle::parse_color(txt_color, rgb))
        {
            colors[i].set(txt_color, rgb);
        }
        else
        {
            const std::string& txt_color = config->opt_string("filament_colour", i);
            if (PresetBundle::parse_color(txt_color, rgb))
                colors[i].set(txt_color, rgb);
        }
    }

    for (GLVolume* volume : volumes)
    {
        if ((volume == nullptr) || volume->is_modifier || volume->is_wipe_tower)
            continue;

        int extruder_id = volume->extruder_id - 1;
        if ((extruder_id < 0) || ((unsigned int)colors.size() <= extruder_id))
            extruder_id = 0;

        const Color& color = colors[extruder_id];
        if (!color.text.empty())
        {
            for (int i = 0; i < 3; ++i)
            {
                volume->color[i] = (float)color.rgb[i] * inv_255;
            }
        }
    }
}

std::vector<double> GLVolumeCollection::get_current_print_zs(bool active_only) const
{
    // Collect layer top positions of all volumes.
    std::vector<double> print_zs;
    for (GLVolume *vol : this->volumes)
    {
        if (!active_only || vol->is_active)
            append(print_zs, vol->print_zs);
    }
    std::sort(print_zs.begin(), print_zs.end());

    // Replace intervals of layers with similar top positions with their average value.
    int n = int(print_zs.size());
    int k = 0;
    for (int i = 0; i < n;) {
        int j = i + 1;
        coordf_t zmax = print_zs[i] + EPSILON;
        for (; j < n && print_zs[j] <= zmax; ++ j) ;
        print_zs[k ++] = (j > i + 1) ? (0.5 * (print_zs[i] + print_zs[j - 1])) : print_zs[i];
        i = j;
    }
    if (k < n)
        print_zs.erase(print_zs.begin() + k, print_zs.end());

    return print_zs;
}

// caller is responsible for supplying NO lines with zero length
static void thick_lines_to_indexed_vertex_array(
    const Lines                 &lines, 
    const std::vector<double>   &widths,
    const std::vector<double>   &heights, 
    bool                         closed,
    double                       top_z,
    GLIndexedVertexArray        &volume)
{
    assert(! lines.empty());
    if (lines.empty())
        return;

#define LEFT    0
#define RIGHT   1
#define TOP     2
#define BOTTOM  3

    // right, left, top, bottom
    int     idx_prev[4]      = { -1, -1, -1, -1 };
    double  bottom_z_prev    = 0.;
    Pointf  b1_prev;
    Vectorf v_prev;
    int     idx_initial[4]   = { -1, -1, -1, -1 };
    double  width_initial    = 0.;
    double  bottom_z_initial = 0.0;

    // loop once more in case of closed loops
    size_t lines_end = closed ? (lines.size() + 1) : lines.size();
    for (size_t ii = 0; ii < lines_end; ++ ii) {
        size_t i = (ii == lines.size()) ? 0 : ii;
        const Line &line = lines[i];
        double len = unscale(line.length());
        double inv_len = 1.0 / len;
        double bottom_z = top_z - heights[i];
        double middle_z = 0.5 * (top_z + bottom_z);
        double width = widths[i];

        bool is_first = (ii == 0);
        bool is_last = (ii == lines_end - 1);
        bool is_closing = closed && is_last;

        Vectorf v = Vectorf::new_unscale(line.vector());
        v.scale(inv_len);

        Pointf a = Pointf::new_unscale(line.a);
        Pointf b = Pointf::new_unscale(line.b);
        Pointf a1 = a;
        Pointf a2 = a;
        Pointf b1 = b;
        Pointf b2 = b;
        {
            double dist = 0.5 * width;  // scaled
            double dx = dist * v.x;
            double dy = dist * v.y;
            a1.translate(+dy, -dx);
            a2.translate(-dy, +dx);
            b1.translate(+dy, -dx);
            b2.translate(-dy, +dx);
        }

        // calculate new XY normals
        Vector n = line.normal();
        Vectorf3 xy_right_normal = Vectorf3::new_unscale(n.x, n.y, 0);
        xy_right_normal.scale(inv_len);

        int idx_a[4];
        int idx_b[4];
        int idx_last = int(volume.vertices_and_normals_interleaved.size() / 6);

        bool bottom_z_different = bottom_z_prev != bottom_z;
        bottom_z_prev = bottom_z;

        if (!is_first && bottom_z_different)
        {
            // Found a change of the layer thickness -> Add a cap at the end of the previous segment.
            volume.push_quad(idx_b[BOTTOM], idx_b[LEFT], idx_b[TOP], idx_b[RIGHT]);
        }

        // Share top / bottom vertices if possible.
        if (is_first) {
            idx_a[TOP] = idx_last++;
            volume.push_geometry(a.x, a.y, top_z   , 0., 0.,  1.); 
        } else {
            idx_a[TOP] = idx_prev[TOP];
        }

        if (is_first || bottom_z_different) {
            // Start of the 1st line segment or a change of the layer thickness while maintaining the print_z.
            idx_a[BOTTOM] = idx_last ++;
            volume.push_geometry(a.x, a.y, bottom_z, 0., 0., -1.);
            idx_a[LEFT ] = idx_last ++;
            volume.push_geometry(a2.x, a2.y, middle_z, -xy_right_normal.x, -xy_right_normal.y, -xy_right_normal.z);
            idx_a[RIGHT] = idx_last ++;
            volume.push_geometry(a1.x, a1.y, middle_z, xy_right_normal.x, xy_right_normal.y, xy_right_normal.z);
        }
        else {
            idx_a[BOTTOM] = idx_prev[BOTTOM];
        }

        if (is_first) {
            // Start of the 1st line segment.
            width_initial    = width;
            bottom_z_initial = bottom_z;
            memcpy(idx_initial, idx_a, sizeof(int) * 4);
        } else {
            // Continuing a previous segment.
            // Share left / right vertices if possible.
			double v_dot    = dot(v_prev, v);
            bool   sharp    = v_dot < 0.707; // sin(45 degrees)
            if (sharp) {
                if (!bottom_z_different)
                {
                    // Allocate new left / right points for the start of this segment as these points will receive their own normals to indicate a sharp turn.
                    idx_a[RIGHT] = idx_last++;
                    volume.push_geometry(a1.x, a1.y, middle_z, xy_right_normal.x, xy_right_normal.y, xy_right_normal.z);
                    idx_a[LEFT] = idx_last++;
                    volume.push_geometry(a2.x, a2.y, middle_z, -xy_right_normal.x, -xy_right_normal.y, -xy_right_normal.z);
                }
            }
            if (v_dot > 0.9) {
                if (!bottom_z_different)
                {
                    // The two successive segments are nearly collinear.
                    idx_a[LEFT ] = idx_prev[LEFT];
                    idx_a[RIGHT] = idx_prev[RIGHT];
                }
            }
            else if (!sharp) {
                if (!bottom_z_different)
                {
                    // Create a sharp corner with an overshot and average the left / right normals.
                    // At the crease angle of 45 degrees, the overshot at the corner will be less than (1-1/cos(PI/8)) = 8.2% over an arc.
                    Pointf intersection;
                    Geometry::ray_ray_intersection(b1_prev, v_prev, a1, v, intersection);
                    a1 = intersection;
                    a2 = 2. * a - intersection;
                    assert(length(a1.vector_to(a)) < width);
                    assert(length(a2.vector_to(a)) < width);
                    float *n_left_prev  = volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT ] * 6;
                    float *p_left_prev  = n_left_prev  + 3;
                    float *n_right_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6;
                    float *p_right_prev = n_right_prev + 3;
                    p_left_prev [0] = float(a2.x);
                    p_left_prev [1] = float(a2.y);
                    p_right_prev[0] = float(a1.x);
                    p_right_prev[1] = float(a1.y);
                    xy_right_normal.x += n_right_prev[0];
                    xy_right_normal.y += n_right_prev[1];
                    xy_right_normal.scale(1. / length(xy_right_normal));
                    n_left_prev [0] = float(-xy_right_normal.x);
                    n_left_prev [1] = float(-xy_right_normal.y);
                    n_right_prev[0] = float( xy_right_normal.x);
                    n_right_prev[1] = float( xy_right_normal.y);
                    idx_a[LEFT ] = idx_prev[LEFT ];
                    idx_a[RIGHT] = idx_prev[RIGHT];
                }
            }
            else if (cross(v_prev, v) > 0.) {
                // Right turn. Fill in the right turn wedge.
                volume.push_triangle(idx_prev[RIGHT], idx_a   [RIGHT],  idx_prev[TOP]   );
                volume.push_triangle(idx_prev[RIGHT], idx_prev[BOTTOM], idx_a   [RIGHT] );
            } else {
                // Left turn. Fill in the left turn wedge.
                volume.push_triangle(idx_prev[LEFT],  idx_prev[TOP],    idx_a   [LEFT]  );
                volume.push_triangle(idx_prev[LEFT],  idx_a   [LEFT],   idx_prev[BOTTOM]);
            }
            if (is_closing) {
                if (!sharp) {
                    if (!bottom_z_different)
                    {
                        // Closing a loop with smooth transition. Unify the closing left / right vertices.
                        memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[LEFT ] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT ] * 6, sizeof(float) * 6);
                        memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[RIGHT] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6, sizeof(float) * 6);
                        volume.vertices_and_normals_interleaved.erase(volume.vertices_and_normals_interleaved.end() - 12, volume.vertices_and_normals_interleaved.end());
                        // Replace the left / right vertex indices to point to the start of the loop. 
                        for (size_t u = volume.quad_indices.size() - 16; u < volume.quad_indices.size(); ++ u) {
                            if (volume.quad_indices[u] == idx_prev[LEFT])
                                volume.quad_indices[u] = idx_initial[LEFT];
                            else if (volume.quad_indices[u] == idx_prev[RIGHT])
                                volume.quad_indices[u] = idx_initial[RIGHT];
                        }
                    }
                }
                // This is the last iteration, only required to solve the transition.
                break;
            }
        }

        // Only new allocate top / bottom vertices, if not closing a loop.
        if (is_closing) {
            idx_b[TOP] = idx_initial[TOP];
        } else {
            idx_b[TOP] = idx_last ++;
            volume.push_geometry(b.x, b.y, top_z   , 0., 0.,  1.);
        }

        if (is_closing && (width == width_initial) && (bottom_z == bottom_z_initial)) {
            idx_b[BOTTOM] = idx_initial[BOTTOM];
        } else {
            idx_b[BOTTOM] = idx_last ++;
            volume.push_geometry(b.x, b.y, bottom_z, 0., 0., -1.);
        }
        // Generate new vertices for the end of this line segment.
        idx_b[LEFT  ] = idx_last ++;
        volume.push_geometry(b2.x, b2.y, middle_z, -xy_right_normal.x, -xy_right_normal.y, -xy_right_normal.z);
        idx_b[RIGHT ] = idx_last ++;
        volume.push_geometry(b1.x, b1.y, middle_z, xy_right_normal.x, xy_right_normal.y, xy_right_normal.z);

        memcpy(idx_prev, idx_b, 4 * sizeof(int));
        bottom_z_prev = bottom_z;
        b1_prev = b1;
        v_prev = v;

        if (bottom_z_different)
        {
            // Found a change of the layer thickness -> Add a cap at the beginning of this segment.
            volume.push_quad(idx_a[BOTTOM], idx_a[RIGHT], idx_a[TOP], idx_a[LEFT]);
        }

        if (! closed) {
            // Terminate open paths with caps.
            if (is_first && !bottom_z_different)
                volume.push_quad(idx_a[BOTTOM], idx_a[RIGHT], idx_a[TOP], idx_a[LEFT]);
            // We don't use 'else' because both cases are true if we have only one line.
            if (is_last && !bottom_z_different)
                volume.push_quad(idx_b[BOTTOM], idx_b[LEFT], idx_b[TOP], idx_b[RIGHT]);
        }

        // Add quads for a straight hollow tube-like segment.
        // bottom-right face
        volume.push_quad(idx_a[BOTTOM], idx_b[BOTTOM], idx_b[RIGHT], idx_a[RIGHT]);
        // top-right face
        volume.push_quad(idx_a[RIGHT], idx_b[RIGHT], idx_b[TOP], idx_a[TOP]);
        // top-left face
        volume.push_quad(idx_a[TOP], idx_b[TOP], idx_b[LEFT], idx_a[LEFT]);
        // bottom-left face
        volume.push_quad(idx_a[LEFT], idx_b[LEFT], idx_b[BOTTOM], idx_a[BOTTOM]);
    }

#undef LEFT
#undef RIGHT
#undef TOP
#undef BOTTOM
}

// caller is responsible for supplying NO lines with zero length
static void thick_lines_to_indexed_vertex_array(const Lines3& lines,
    const std::vector<double>& widths,
    const std::vector<double>& heights,
    bool closed,
    GLIndexedVertexArray& volume)
{
    assert(!lines.empty());
    if (lines.empty())
        return;

#define LEFT    0
#define RIGHT   1
#define TOP     2
#define BOTTOM  3

    // left, right, top, bottom
    int      idx_initial[4] = { -1, -1, -1, -1 };
    int      idx_prev[4] = { -1, -1, -1, -1 };
    double   z_prev = 0.0;
    Vectorf3 n_right_prev;
    Vectorf3 n_top_prev;
    Vectorf3 unit_v_prev;
    double   width_initial = 0.0;

    // new vertices around the line endpoints
    // left, right, top, bottom
    Pointf3 a[4];
    Pointf3 b[4];

    // loop once more in case of closed loops
    size_t lines_end = closed ? (lines.size() + 1) : lines.size();
    for (size_t ii = 0; ii < lines_end; ++ii)
    {
        size_t i = (ii == lines.size()) ? 0 : ii;

        const Line3& line = lines[i];
        double height = heights[i];
        double width = widths[i];

        Vectorf3 unit_v = normalize(Vectorf3::new_unscale(line.vector()));

        Vectorf3 n_top;
        Vectorf3 n_right;
        Vectorf3 unit_positive_z(0.0, 0.0, 1.0);

        if ((line.a.x == line.b.x) && (line.a.y == line.b.y))
        {
            // vertical segment
            n_right = (line.a.z < line.b.z) ? Vectorf3(-1.0, 0.0, 0.0) : Vectorf3(1.0, 0.0, 0.0);
            n_top = Vectorf3(0.0, 1.0, 0.0);
        }
        else
        {
            // generic segment
            n_right = normalize(cross(unit_v, unit_positive_z));
            n_top = normalize(cross(n_right, unit_v));
        }

        Vectorf3 rl_displacement = 0.5 * width * n_right;
        Vectorf3 tb_displacement = 0.5 * height * n_top;
        Pointf3 l_a = Pointf3::new_unscale(line.a);
        Pointf3 l_b = Pointf3::new_unscale(line.b);

        a[RIGHT] = l_a + rl_displacement;
        a[LEFT] = l_a - rl_displacement;
        a[TOP] = l_a + tb_displacement;
        a[BOTTOM] = l_a - tb_displacement;
        b[RIGHT] = l_b + rl_displacement;
        b[LEFT] = l_b - rl_displacement;
        b[TOP] = l_b + tb_displacement;
        b[BOTTOM] = l_b - tb_displacement;

        Vectorf3 n_bottom = -n_top;
        Vectorf3 n_left = -n_right;

        int idx_a[4];
        int idx_b[4];
        int idx_last = int(volume.vertices_and_normals_interleaved.size() / 6);

        bool z_different = (z_prev != l_a.z);
        z_prev = l_b.z;

        // Share top / bottom vertices if possible.
        if (ii == 0)
        {
            idx_a[TOP] = idx_last++;
            volume.push_geometry(a[TOP], n_top);
        }
        else
            idx_a[TOP] = idx_prev[TOP];

        if ((ii == 0) || z_different)
        {
            // Start of the 1st line segment or a change of the layer thickness while maintaining the print_z.
            idx_a[BOTTOM] = idx_last++;
            volume.push_geometry(a[BOTTOM], n_bottom);
            idx_a[LEFT] = idx_last++;
            volume.push_geometry(a[LEFT], n_left);
            idx_a[RIGHT] = idx_last++;
            volume.push_geometry(a[RIGHT], n_right);
        }
        else
            idx_a[BOTTOM] = idx_prev[BOTTOM];

        if (ii == 0)
        {
            // Start of the 1st line segment.
            width_initial = width;
            ::memcpy(idx_initial, idx_a, sizeof(int) * 4);
        }
        else
        {
            // Continuing a previous segment.
            // Share left / right vertices if possible.
            double v_dot = dot(unit_v_prev, unit_v);
            bool is_sharp = v_dot < 0.707; // sin(45 degrees)
            bool is_right_turn = dot(n_top_prev, cross(unit_v_prev, unit_v)) > 0.0;

            if (is_sharp)
            {
                // Allocate new left / right points for the start of this segment as these points will receive their own normals to indicate a sharp turn.
                idx_a[RIGHT] = idx_last++;
                volume.push_geometry(a[RIGHT], n_right);
                idx_a[LEFT] = idx_last++;
                volume.push_geometry(a[LEFT], n_left);
            }

            if (v_dot > 0.9)
            {
                // The two successive segments are nearly collinear.
                idx_a[LEFT] = idx_prev[LEFT];
                idx_a[RIGHT] = idx_prev[RIGHT];
            }
            else if (!is_sharp)
            {
                // Create a sharp corner with an overshot and average the left / right normals.
                // At the crease angle of 45 degrees, the overshot at the corner will be less than (1-1/cos(PI/8)) = 8.2% over an arc.

                // averages normals
                Vectorf3 average_n_right = normalize(0.5 * (n_right + n_right_prev));
                Vectorf3 average_n_left = -average_n_right;
                Vectorf3 average_rl_displacement = 0.5 * width * average_n_right;

                // updates vertices around a
                a[RIGHT] = l_a + average_rl_displacement;
                a[LEFT] = l_a - average_rl_displacement;

                // updates previous line normals
                float* normal_left_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT] * 6;
                normal_left_prev[0] = float(average_n_left.x);
                normal_left_prev[1] = float(average_n_left.y);
                normal_left_prev[2] = float(average_n_left.z);

                float* normal_right_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6;
                normal_right_prev[0] = float(average_n_right.x);
                normal_right_prev[1] = float(average_n_right.y);
                normal_right_prev[2] = float(average_n_right.z);

                // updates previous line's vertices around b
                float* b_left_prev = normal_left_prev + 3;
                b_left_prev[0] = float(a[LEFT].x);
                b_left_prev[1] = float(a[LEFT].y);
                b_left_prev[2] = float(a[LEFT].z);

                float* b_right_prev = normal_right_prev + 3;
                b_right_prev[0] = float(a[RIGHT].x);
                b_right_prev[1] = float(a[RIGHT].y);
                b_right_prev[2] = float(a[RIGHT].z);

                idx_a[LEFT] = idx_prev[LEFT];
                idx_a[RIGHT] = idx_prev[RIGHT];
            }
            else if (is_right_turn)
            {
                // Right turn. Fill in the right turn wedge.
                volume.push_triangle(idx_prev[RIGHT], idx_a[RIGHT], idx_prev[TOP]);
                volume.push_triangle(idx_prev[RIGHT], idx_prev[BOTTOM], idx_a[RIGHT]);
            }
            else
            {
                // Left turn. Fill in the left turn wedge.
                volume.push_triangle(idx_prev[LEFT], idx_prev[TOP], idx_a[LEFT]);
                volume.push_triangle(idx_prev[LEFT], idx_a[LEFT], idx_prev[BOTTOM]);
            }

            if (ii == lines.size())
            {
                if (!is_sharp)
                {
                    // Closing a loop with smooth transition. Unify the closing left / right vertices.
                    ::memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[LEFT] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT] * 6, sizeof(float) * 6);
                    ::memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[RIGHT] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6, sizeof(float) * 6);
                    volume.vertices_and_normals_interleaved.erase(volume.vertices_and_normals_interleaved.end() - 12, volume.vertices_and_normals_interleaved.end());
                    // Replace the left / right vertex indices to point to the start of the loop. 
                    for (size_t u = volume.quad_indices.size() - 16; u < volume.quad_indices.size(); ++u)
                    {
                        if (volume.quad_indices[u] == idx_prev[LEFT])
                            volume.quad_indices[u] = idx_initial[LEFT];
                        else if (volume.quad_indices[u] == idx_prev[RIGHT])
                            volume.quad_indices[u] = idx_initial[RIGHT];
                    }
                }

                // This is the last iteration, only required to solve the transition.
                break;
            }
        }

        // Only new allocate top / bottom vertices, if not closing a loop.
        if (closed && (ii + 1 == lines.size()))
            idx_b[TOP] = idx_initial[TOP];
        else
        {
            idx_b[TOP] = idx_last++;
            volume.push_geometry(b[TOP], n_top);
        }

        if (closed && (ii + 1 == lines.size()) && (width == width_initial))
            idx_b[BOTTOM] = idx_initial[BOTTOM];
        else
        {
            idx_b[BOTTOM] = idx_last++;
            volume.push_geometry(b[BOTTOM], n_bottom);
        }

        // Generate new vertices for the end of this line segment.
        idx_b[LEFT] = idx_last++;
        volume.push_geometry(b[LEFT], n_left);
        idx_b[RIGHT] = idx_last++;
        volume.push_geometry(b[RIGHT], n_right);

        ::memcpy(idx_prev, idx_b, 4 * sizeof(int));
        n_right_prev = n_right;
        n_top_prev = n_top;
        unit_v_prev = unit_v;

        if (!closed)
        {
            // Terminate open paths with caps.
            if (i == 0)
                volume.push_quad(idx_a[BOTTOM], idx_a[RIGHT], idx_a[TOP], idx_a[LEFT]);

            // We don't use 'else' because both cases are true if we have only one line.
            if (i + 1 == lines.size())
                volume.push_quad(idx_b[BOTTOM], idx_b[LEFT], idx_b[TOP], idx_b[RIGHT]);
        }

        // Add quads for a straight hollow tube-like segment.
        // bottom-right face
        volume.push_quad(idx_a[BOTTOM], idx_b[BOTTOM], idx_b[RIGHT], idx_a[RIGHT]);
        // top-right face
        volume.push_quad(idx_a[RIGHT], idx_b[RIGHT], idx_b[TOP], idx_a[TOP]);
        // top-left face
        volume.push_quad(idx_a[TOP], idx_b[TOP], idx_b[LEFT], idx_a[LEFT]);
        // bottom-left face
        volume.push_quad(idx_a[LEFT], idx_b[LEFT], idx_b[BOTTOM], idx_a[BOTTOM]);
    }

#undef LEFT
#undef RIGHT
#undef TOP
#undef BOTTOM
}

static void point_to_indexed_vertex_array(const Point3& point,
    double width,
    double height,
    GLIndexedVertexArray& volume)
{
    // builds a double piramid, with vertices on the local axes, around the point

    Pointf3 center = Pointf3::new_unscale(point);

    double scale_factor = 1.0;
    double w = scale_factor * width;
    double h = scale_factor * height;

    // new vertices ids
    int idx_last = int(volume.vertices_and_normals_interleaved.size() / 6);
    int idxs[6];
    for (int i = 0; i < 6; ++i)
    {
        idxs[i] = idx_last + i;
    }

    Vectorf3 displacement_x(w, 0.0, 0.0);
    Vectorf3 displacement_y(0.0, w, 0.0);
    Vectorf3 displacement_z(0.0, 0.0, h);

    Vectorf3 unit_x(1.0, 0.0, 0.0);
    Vectorf3 unit_y(0.0, 1.0, 0.0);
    Vectorf3 unit_z(0.0, 0.0, 1.0);

    // vertices
    volume.push_geometry(center - displacement_x, -unit_x); // idxs[0]
    volume.push_geometry(center + displacement_x, unit_x);  // idxs[1]
    volume.push_geometry(center - displacement_y, -unit_y); // idxs[2]
    volume.push_geometry(center + displacement_y, unit_y);  // idxs[3]
    volume.push_geometry(center - displacement_z, -unit_z); // idxs[4]
    volume.push_geometry(center + displacement_z, unit_z);  // idxs[5]

    // top piramid faces
    volume.push_triangle(idxs[0], idxs[2], idxs[5]);
    volume.push_triangle(idxs[2], idxs[1], idxs[5]);
    volume.push_triangle(idxs[1], idxs[3], idxs[5]);
    volume.push_triangle(idxs[3], idxs[0], idxs[5]);

    // bottom piramid faces
    volume.push_triangle(idxs[2], idxs[0], idxs[4]);
    volume.push_triangle(idxs[1], idxs[2], idxs[4]);
    volume.push_triangle(idxs[3], idxs[1], idxs[4]);
    volume.push_triangle(idxs[0], idxs[3], idxs[4]);
}

void _3DScene::thick_lines_to_verts(
    const Lines                 &lines,
    const std::vector<double>   &widths,
    const std::vector<double>   &heights, 
    bool                         closed,
    double                       top_z,
    GLVolume                    &volume)
{
    thick_lines_to_indexed_vertex_array(lines, widths, heights, closed, top_z, volume.indexed_vertex_array);
}

void _3DScene::thick_lines_to_verts(const Lines3& lines,
    const std::vector<double>& widths,
    const std::vector<double>& heights,
    bool closed,
    GLVolume& volume)
{
    thick_lines_to_indexed_vertex_array(lines, widths, heights, closed, volume.indexed_vertex_array);
}

static void thick_point_to_verts(const Point3& point,
    double width,
    double height,
    GLVolume& volume)
{
    point_to_indexed_vertex_array(point, width, height, volume.indexed_vertex_array);
}

// Fill in the qverts and tverts with quads and triangles for the extrusion_path.
void _3DScene::extrusionentity_to_verts(const ExtrusionPath &extrusion_path, float print_z, GLVolume &volume)
{
    Lines               lines = extrusion_path.polyline.lines();
    std::vector<double> widths(lines.size(), extrusion_path.width);
    std::vector<double> heights(lines.size(), extrusion_path.height);
    thick_lines_to_verts(lines, widths, heights, false, print_z, volume);
}

// Fill in the qverts and tverts with quads and triangles for the extrusion_path.
void _3DScene::extrusionentity_to_verts(const ExtrusionPath &extrusion_path, float print_z, const Point &copy, GLVolume &volume)
{
    Polyline            polyline = extrusion_path.polyline;
    polyline.remove_duplicate_points();
    polyline.translate(copy);
    Lines               lines = polyline.lines();
    std::vector<double> widths(lines.size(), extrusion_path.width);
    std::vector<double> heights(lines.size(), extrusion_path.height);
    thick_lines_to_verts(lines, widths, heights, false, print_z, volume);
}

// Fill in the qverts and tverts with quads and triangles for the extrusion_loop.
void _3DScene::extrusionentity_to_verts(const ExtrusionLoop &extrusion_loop, float print_z, const Point &copy, GLVolume &volume)
{
    Lines               lines;
    std::vector<double> widths;
    std::vector<double> heights;
    for (const ExtrusionPath &extrusion_path : extrusion_loop.paths) {
        Polyline            polyline = extrusion_path.polyline;
        polyline.remove_duplicate_points();
        polyline.translate(copy);
        Lines lines_this = polyline.lines();
        append(lines, lines_this);
        widths.insert(widths.end(), lines_this.size(), extrusion_path.width);
        heights.insert(heights.end(), lines_this.size(), extrusion_path.height);
    }
    thick_lines_to_verts(lines, widths, heights, true, print_z, volume);
}

// Fill in the qverts and tverts with quads and triangles for the extrusion_multi_path.
void _3DScene::extrusionentity_to_verts(const ExtrusionMultiPath &extrusion_multi_path, float print_z, const Point &copy, GLVolume &volume)
{
    Lines               lines;
    std::vector<double> widths;
    std::vector<double> heights;
    for (const ExtrusionPath &extrusion_path : extrusion_multi_path.paths) {
        Polyline            polyline = extrusion_path.polyline;
        polyline.remove_duplicate_points();
        polyline.translate(copy);
        Lines lines_this = polyline.lines();
        append(lines, lines_this);
        widths.insert(widths.end(), lines_this.size(), extrusion_path.width);
        heights.insert(heights.end(), lines_this.size(), extrusion_path.height);
    }
    thick_lines_to_verts(lines, widths, heights, false, print_z, volume);
}

void _3DScene::extrusionentity_to_verts(const ExtrusionEntityCollection &extrusion_entity_collection, float print_z, const Point &copy, GLVolume &volume)
{
    for (const ExtrusionEntity *extrusion_entity : extrusion_entity_collection.entities)
        extrusionentity_to_verts(extrusion_entity, print_z, copy, volume);
}

void _3DScene::extrusionentity_to_verts(const ExtrusionEntity *extrusion_entity, float print_z, const Point &copy, GLVolume &volume)
{
    if (extrusion_entity != nullptr) {
        auto *extrusion_path = dynamic_cast<const ExtrusionPath*>(extrusion_entity);
        if (extrusion_path != nullptr)
            extrusionentity_to_verts(*extrusion_path, print_z, copy, volume);
        else {
            auto *extrusion_loop = dynamic_cast<const ExtrusionLoop*>(extrusion_entity);
            if (extrusion_loop != nullptr)
                extrusionentity_to_verts(*extrusion_loop, print_z, copy, volume);
            else {
                auto *extrusion_multi_path = dynamic_cast<const ExtrusionMultiPath*>(extrusion_entity);
                if (extrusion_multi_path != nullptr)
                    extrusionentity_to_verts(*extrusion_multi_path, print_z, copy, volume);
                else {
                    auto *extrusion_entity_collection = dynamic_cast<const ExtrusionEntityCollection*>(extrusion_entity);
                    if (extrusion_entity_collection != nullptr)
                        extrusionentity_to_verts(*extrusion_entity_collection, print_z, copy, volume);
                    else {
                        CONFESS("Unexpected extrusion_entity type in to_verts()");
                    }
                }
            }
        }
    }
}

void _3DScene::polyline3_to_verts(const Polyline3& polyline, double width, double height, GLVolume& volume)
{
    Lines3 lines = polyline.lines();
    std::vector<double> widths(lines.size(), width);
    std::vector<double> heights(lines.size(), height);
    thick_lines_to_verts(lines, widths, heights, false, volume);
}

void _3DScene::point3_to_verts(const Point3& point, double width, double height, GLVolume& volume)
{
    thick_point_to_verts(point, width, height, volume);
}

_3DScene::LegendTexture _3DScene::s_legend_texture;
_3DScene::WarningTexture _3DScene::s_warning_texture;
GUI::GLCanvas3DManager _3DScene::s_canvas_mgr;

unsigned int _3DScene::TextureBase::finalize()
{
    if (!m_data.empty()) {
        // sends buffer to gpu
        ::glGenTextures(1, &m_tex_id);
        ::glBindTexture(GL_TEXTURE_2D, m_tex_id);
        ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei)m_tex_width, (GLsizei)m_tex_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid*)m_data.data());
        ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
        ::glBindTexture(GL_TEXTURE_2D, 0);
        m_data.clear();
    }
    return (m_tex_width > 0 && m_tex_height > 0) ? m_tex_id : 0;
}

void _3DScene::TextureBase::_destroy_texture()
{
    if (m_tex_id > 0)
    {
        ::glDeleteTextures(1, &m_tex_id);
        m_tex_id = 0;
        m_tex_height = 0;
        m_tex_width = 0;
    }
    m_data.clear();
}


const unsigned char _3DScene::WarningTexture::Background_Color[3] = { 9, 91, 134 };
const unsigned char _3DScene::WarningTexture::Opacity = 255;

// Generate a texture data, but don't load it into the GPU yet, as the GPU context may not yet be valid.
bool _3DScene::WarningTexture::generate(const std::string& msg)
{
    // Mark the texture as released, but don't release the texture from the GPU yet.
    m_tex_width = m_tex_height = 0;
    m_data.clear();

    if (msg.empty())
        return false;

    wxMemoryDC memDC;
    // select default font
    memDC.SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT));

    // calculates texture size
    wxCoord w, h;
    memDC.GetTextExtent(msg, &w, &h);
    m_tex_width = (unsigned int)w;
    m_tex_height = (unsigned int)h;

    // generates bitmap
    wxBitmap bitmap(m_tex_width, m_tex_height);

#if defined(__APPLE__) || defined(_MSC_VER)
    bitmap.UseAlpha();
#endif

    memDC.SelectObject(bitmap);
    memDC.SetBackground(wxBrush(wxColour(Background_Color[0], Background_Color[1], Background_Color[2])));
    memDC.Clear();

    memDC.SetTextForeground(*wxWHITE);

    // draw message
    memDC.DrawText(msg, 0, 0);

    memDC.SelectObject(wxNullBitmap);

    // Convert the bitmap into a linear data ready to be loaded into the GPU.
    {
        wxImage image = bitmap.ConvertToImage();
        image.SetMaskColour(Background_Color[0], Background_Color[1], Background_Color[2]);

        // prepare buffer
        m_data.assign(4 * m_tex_width * m_tex_height, 0);
        for (unsigned int h = 0; h < m_tex_height; ++h)
        {
            unsigned int hh = h * m_tex_width;
            unsigned char* px_ptr = m_data.data() + 4 * hh;
            for (unsigned int w = 0; w < m_tex_width; ++w)
            {
                *px_ptr++ = image.GetRed(w, h);
                *px_ptr++ = image.GetGreen(w, h);
                *px_ptr++ = image.GetBlue(w, h);
                *px_ptr++ = image.IsTransparent(w, h) ? 0 : Opacity;
            }
        }
    }
    return true;
}

const unsigned char _3DScene::LegendTexture::Squares_Border_Color[3] = { 64, 64, 64 };
const unsigned char _3DScene::LegendTexture::Background_Color[3] = { 9, 91, 134 };
const unsigned char _3DScene::LegendTexture::Opacity = 255;

// Generate a texture data, but don't load it into the GPU yet, as the GPU context may not yet be valid.
bool _3DScene::LegendTexture::generate(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
{
    // Mark the texture as released, but don't release the texture from the GPU yet.
    m_tex_width = m_tex_height = 0;
    m_data.clear();

    // collects items to render
    auto title = GUI::L_str(preview_data.get_legend_title());
    const GCodePreviewData::LegendItemsList& items = preview_data.get_legend_items(tool_colors);

    unsigned int items_count = (unsigned int)items.size();
    if (items_count == 0)
        // nothing to render, return
        return false;

    wxMemoryDC memDC;
    // select default font
    memDC.SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT));

    // calculates texture size
    wxCoord w, h;
    memDC.GetTextExtent(title, &w, &h);
    unsigned int title_width = (unsigned int)w;
    unsigned int title_height = (unsigned int)h;

    unsigned int max_text_width = 0;
    unsigned int max_text_height = 0;
    for (const GCodePreviewData::LegendItem& item : items)
    {
        memDC.GetTextExtent(GUI::from_u8(item.text), &w, &h);
        max_text_width = std::max(max_text_width, (unsigned int)w);
        max_text_height = std::max(max_text_height, (unsigned int)h);
    }

    m_tex_width = std::max(2 * Px_Border + title_width, 2 * (Px_Border + Px_Square_Contour) + Px_Square + Px_Text_Offset + max_text_width);
    m_tex_height = 2 * (Px_Border + Px_Square_Contour) + title_height + Px_Title_Offset + items_count * Px_Square;
    if (items_count > 1)
        m_tex_height += (items_count - 1) * Px_Square_Contour;

    // generates bitmap
    wxBitmap bitmap(m_tex_width, m_tex_height);

#if defined(__APPLE__) || defined(_MSC_VER)
    bitmap.UseAlpha();
#endif

    memDC.SelectObject(bitmap);
    memDC.SetBackground(wxBrush(wxColour(Background_Color[0], Background_Color[1], Background_Color[2])));
    memDC.Clear();

    memDC.SetTextForeground(*wxWHITE);

    // draw title
    unsigned int title_x = Px_Border;
    unsigned int title_y = Px_Border;
    memDC.DrawText(title, title_x, title_y);

    // draw icons contours as background
    unsigned int squares_contour_x = Px_Border;
    unsigned int squares_contour_y = Px_Border + title_height + Px_Title_Offset;
    unsigned int squares_contour_width = Px_Square + 2 * Px_Square_Contour;
    unsigned int squares_contour_height = items_count * Px_Square + 2 * Px_Square_Contour;
    if (items_count > 1)
        squares_contour_height += (items_count - 1) * Px_Square_Contour;

    wxColour color(Squares_Border_Color[0], Squares_Border_Color[1], Squares_Border_Color[2]);
    wxPen pen(color);
    wxBrush brush(color);
    memDC.SetPen(pen);
    memDC.SetBrush(brush);
    memDC.DrawRectangle(wxRect(squares_contour_x, squares_contour_y, squares_contour_width, squares_contour_height));

    // draw items (colored icon + text)
    unsigned int icon_x = squares_contour_x + Px_Square_Contour;
    unsigned int icon_x_inner = icon_x + 1;
    unsigned int icon_y = squares_contour_y + Px_Square_Contour;
    unsigned int icon_y_step = Px_Square + Px_Square_Contour;

    unsigned int text_x = icon_x + Px_Square + Px_Text_Offset;
    unsigned int text_y_offset = (Px_Square - max_text_height) / 2;

    unsigned int px_inner_square = Px_Square - 2;

    for (const GCodePreviewData::LegendItem& item : items)
    {
        // draw darker icon perimeter
        const std::vector<unsigned char>& item_color_bytes = item.color.as_bytes();
        wxImage::HSVValue dark_hsv = wxImage::RGBtoHSV(wxImage::RGBValue(item_color_bytes[0], item_color_bytes[1], item_color_bytes[2]));
        dark_hsv.value *= 0.75;
        wxImage::RGBValue dark_rgb = wxImage::HSVtoRGB(dark_hsv);
        color.Set(dark_rgb.red, dark_rgb.green, dark_rgb.blue, item_color_bytes[3]);
        pen.SetColour(color);
        brush.SetColour(color);
        memDC.SetPen(pen);
        memDC.SetBrush(brush);
        memDC.DrawRectangle(wxRect(icon_x, icon_y, Px_Square, Px_Square));

        // draw icon interior
        color.Set(item_color_bytes[0], item_color_bytes[1], item_color_bytes[2], item_color_bytes[3]);
        pen.SetColour(color);
        brush.SetColour(color);
        memDC.SetPen(pen);
        memDC.SetBrush(brush);
        memDC.DrawRectangle(wxRect(icon_x_inner, icon_y + 1, px_inner_square, px_inner_square));

        // draw text
		memDC.DrawText(GUI::from_u8(item.text), text_x, icon_y + text_y_offset);

        // update y
        icon_y += icon_y_step;
    }

    memDC.SelectObject(wxNullBitmap);

    // Convert the bitmap into a linear data ready to be loaded into the GPU.
    {
        wxImage image = bitmap.ConvertToImage();
        image.SetMaskColour(Background_Color[0], Background_Color[1], Background_Color[2]);

        // prepare buffer
        m_data.assign(4 * m_tex_width * m_tex_height, 0);
        for (unsigned int h = 0; h < m_tex_height; ++h)
        {
            unsigned int hh = h * m_tex_width;
            unsigned char* px_ptr = m_data.data() + 4 * hh;
            for (unsigned int w = 0; w < m_tex_width; ++w)
            {
                *px_ptr++ = image.GetRed(w, h);
                *px_ptr++ = image.GetGreen(w, h);
                *px_ptr++ = image.GetBlue(w, h);
                *px_ptr++ = image.IsTransparent(w, h) ? 0 : Opacity;
            }
        }
    }
    return true;
}

void _3DScene::init_gl()
{
    s_canvas_mgr.init_gl();
}

std::string _3DScene::get_gl_info(bool format_as_html, bool extensions)
{
    return s_canvas_mgr.get_gl_info(format_as_html, extensions);
}

bool _3DScene::use_VBOs()
{
    return s_canvas_mgr.use_VBOs();
}

bool _3DScene::add_canvas(wxGLCanvas* canvas)
{
    return s_canvas_mgr.add(canvas);
}

bool _3DScene::remove_canvas(wxGLCanvas* canvas)
{
    return s_canvas_mgr.remove(canvas);
}

void _3DScene::remove_all_canvases()
{
    s_canvas_mgr.remove_all();
}

bool _3DScene::init(wxGLCanvas* canvas)
{
    return s_canvas_mgr.init(canvas);
}

void _3DScene::set_as_dirty(wxGLCanvas* canvas)
{
    s_canvas_mgr.set_as_dirty(canvas);
}

unsigned int _3DScene::get_volumes_count(wxGLCanvas* canvas)
{
    return s_canvas_mgr.get_volumes_count(canvas);
}

void _3DScene::reset_volumes(wxGLCanvas* canvas)
{
    s_canvas_mgr.reset_volumes(canvas);
}

void _3DScene::deselect_volumes(wxGLCanvas* canvas)
{
    s_canvas_mgr.deselect_volumes(canvas);
}

void _3DScene::select_volume(wxGLCanvas* canvas, unsigned int id)
{
    s_canvas_mgr.select_volume(canvas, id);
}

void _3DScene::update_volumes_selection(wxGLCanvas* canvas, const std::vector<int>& selections)
{
    s_canvas_mgr.update_volumes_selection(canvas, selections);
}

bool _3DScene::check_volumes_outside_state(wxGLCanvas* canvas, const DynamicPrintConfig* config)
{
    return s_canvas_mgr.check_volumes_outside_state(canvas, config);
}

bool _3DScene::move_volume_up(wxGLCanvas* canvas, unsigned int id)
{
    return s_canvas_mgr.move_volume_up(canvas, id);
}

bool _3DScene::move_volume_down(wxGLCanvas* canvas, unsigned int id)
{
    return s_canvas_mgr.move_volume_down(canvas, id);
}

void _3DScene::set_objects_selections(wxGLCanvas* canvas, const std::vector<int>& selections)
{
    s_canvas_mgr.set_objects_selections(canvas, selections);
}

void _3DScene::set_config(wxGLCanvas* canvas, DynamicPrintConfig* config)
{
    s_canvas_mgr.set_config(canvas, config);
}

void _3DScene::set_print(wxGLCanvas* canvas, Print* print)
{
    s_canvas_mgr.set_print(canvas, print);
}

void _3DScene::set_model(wxGLCanvas* canvas, Model* model)
{
    s_canvas_mgr.set_model(canvas, model);
}

void _3DScene::set_bed_shape(wxGLCanvas* canvas, const Pointfs& shape)
{
    return s_canvas_mgr.set_bed_shape(canvas, shape);
}

void _3DScene::set_auto_bed_shape(wxGLCanvas* canvas)
{
    return s_canvas_mgr.set_auto_bed_shape(canvas);
}

BoundingBoxf3 _3DScene::get_volumes_bounding_box(wxGLCanvas* canvas)
{
    return s_canvas_mgr.get_volumes_bounding_box(canvas);
}

void _3DScene::set_axes_length(wxGLCanvas* canvas, float length)
{
    s_canvas_mgr.set_axes_length(canvas, length);
}

void _3DScene::set_cutting_plane(wxGLCanvas* canvas, float z, const ExPolygons& polygons)
{
    return s_canvas_mgr.set_cutting_plane(canvas, z, polygons);
}

void _3DScene::set_color_by(wxGLCanvas* canvas, const std::string& value)
{
    return s_canvas_mgr.set_color_by(canvas, value);
}

void _3DScene::set_select_by(wxGLCanvas* canvas, const std::string& value)
{
    return s_canvas_mgr.set_select_by(canvas, value);
}

void _3DScene::set_drag_by(wxGLCanvas* canvas, const std::string& value)
{
    return s_canvas_mgr.set_drag_by(canvas, value);
}

bool _3DScene::is_layers_editing_enabled(wxGLCanvas* canvas)
{
    return s_canvas_mgr.is_layers_editing_enabled(canvas);
}

bool _3DScene::is_layers_editing_allowed(wxGLCanvas* canvas)
{
    return s_canvas_mgr.is_layers_editing_allowed(canvas);
}

bool _3DScene::is_shader_enabled(wxGLCanvas* canvas)
{
    return s_canvas_mgr.is_shader_enabled(canvas);
}

bool _3DScene::is_reload_delayed(wxGLCanvas* canvas)
{
    return s_canvas_mgr.is_reload_delayed(canvas);
}

void _3DScene::enable_layers_editing(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_layers_editing(canvas, enable);
}

void _3DScene::enable_warning_texture(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_warning_texture(canvas, enable);
}

void _3DScene::enable_legend_texture(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_legend_texture(canvas, enable);
}

void _3DScene::enable_picking(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_picking(canvas, enable);
}

void _3DScene::enable_moving(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_moving(canvas, enable);
}

void _3DScene::enable_gizmos(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_gizmos(canvas, enable);
}

void _3DScene::enable_shader(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_shader(canvas, enable);
}

void _3DScene::enable_force_zoom_to_bed(wxGLCanvas* canvas, bool enable)
{
    s_canvas_mgr.enable_force_zoom_to_bed(canvas, enable);
}

void _3DScene::allow_multisample(wxGLCanvas* canvas, bool allow)
{
    s_canvas_mgr.allow_multisample(canvas, allow);
}

void _3DScene::zoom_to_bed(wxGLCanvas* canvas)
{
    s_canvas_mgr.zoom_to_bed(canvas);
}

void _3DScene::zoom_to_volumes(wxGLCanvas* canvas)
{
    s_canvas_mgr.zoom_to_volumes(canvas);
}

void _3DScene::select_view(wxGLCanvas* canvas, const std::string& direction)
{
    s_canvas_mgr.select_view(canvas, direction);
}

void _3DScene::set_viewport_from_scene(wxGLCanvas* canvas, wxGLCanvas* other)
{
    s_canvas_mgr.set_viewport_from_scene(canvas, other);
}

void _3DScene::update_volumes_colors_by_extruder(wxGLCanvas* canvas)
{
    s_canvas_mgr.update_volumes_colors_by_extruder(canvas);
}

void _3DScene::render(wxGLCanvas* canvas)
{
    s_canvas_mgr.render(canvas);
}

std::vector<double> _3DScene::get_current_print_zs(wxGLCanvas* canvas, bool active_only)
{
    return s_canvas_mgr.get_current_print_zs(canvas, active_only);
}

void _3DScene::set_toolpaths_range(wxGLCanvas* canvas, double low, double high)
{
    s_canvas_mgr.set_toolpaths_range(canvas, low, high);
}

void _3DScene::register_on_viewport_changed_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_viewport_changed_callback(canvas, callback);
}

void _3DScene::register_on_double_click_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_double_click_callback(canvas, callback);
}

void _3DScene::register_on_right_click_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_right_click_callback(canvas, callback);
}

void _3DScene::register_on_select_object_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_select_object_callback(canvas, callback);
}

void _3DScene::register_on_model_update_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_model_update_callback(canvas, callback);
}

void _3DScene::register_on_remove_object_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_remove_object_callback(canvas, callback);
}

void _3DScene::register_on_arrange_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_arrange_callback(canvas, callback);
}

void _3DScene::register_on_rotate_object_left_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_rotate_object_left_callback(canvas, callback);
}

void _3DScene::register_on_rotate_object_right_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_rotate_object_right_callback(canvas, callback);
}

void _3DScene::register_on_scale_object_uniformly_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_scale_object_uniformly_callback(canvas, callback);
}

void _3DScene::register_on_increase_objects_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_increase_objects_callback(canvas, callback);
}

void _3DScene::register_on_decrease_objects_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_decrease_objects_callback(canvas, callback);
}

void _3DScene::register_on_instance_moved_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_instance_moved_callback(canvas, callback);
}

void _3DScene::register_on_wipe_tower_moved_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_wipe_tower_moved_callback(canvas, callback);
}

void _3DScene::register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_enable_action_buttons_callback(canvas, callback);
}

void _3DScene::register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback)
{
    s_canvas_mgr.register_on_gizmo_scale_uniformly_callback(canvas, callback);
}

static inline int hex_digit_to_int(const char c)
{
    return 
        (c >= '0' && c <= '9') ? int(c - '0') : 
        (c >= 'A' && c <= 'F') ? int(c - 'A') + 10 :
		(c >= 'a' && c <= 'f') ? int(c - 'a') + 10 : -1;
}

static inline std::vector<float> parse_colors(const std::vector<std::string> &scolors)
{
    std::vector<float> output(scolors.size() * 4, 1.f);
    for (size_t i = 0; i < scolors.size(); ++ i) {
        const std::string &scolor = scolors[i];
        const char        *c      = scolor.data() + 1;
        if (scolor.size() == 7 && scolor.front() == '#') {
			for (size_t j = 0; j < 3; ++j) {
                int digit1 = hex_digit_to_int(*c ++);
                int digit2 = hex_digit_to_int(*c ++);
                if (digit1 == -1 || digit2 == -1)
                    break;
                output[i * 4 + j] = float(digit1 * 16 + digit2) / 255.f;
            }
        }
    }
    return output;
}

std::vector<int> _3DScene::load_object(wxGLCanvas* canvas, const ModelObject* model_object, int obj_idx, std::vector<int> instance_idxs)
{
    return s_canvas_mgr.load_object(canvas, model_object, obj_idx, instance_idxs);
}

std::vector<int> _3DScene::load_object(wxGLCanvas* canvas, const Model* model, int obj_idx)
{
    return s_canvas_mgr.load_object(canvas, model, obj_idx);
}

void _3DScene::reload_scene(wxGLCanvas* canvas, bool force)
{
    s_canvas_mgr.reload_scene(canvas, force);
}

void _3DScene::load_print_toolpaths(wxGLCanvas* canvas)
{
    s_canvas_mgr.load_print_toolpaths(canvas);
}

void _3DScene::load_print_object_toolpaths(wxGLCanvas* canvas, const PrintObject* print_object, const std::vector<std::string>& str_tool_colors)
{
    s_canvas_mgr.load_print_object_toolpaths(canvas, print_object, str_tool_colors);
}

void _3DScene::load_wipe_tower_toolpaths(wxGLCanvas* canvas, const std::vector<std::string>& str_tool_colors)
{
    s_canvas_mgr.load_wipe_tower_toolpaths(canvas, str_tool_colors);
}

void _3DScene::load_gcode_preview(wxGLCanvas* canvas, const GCodePreviewData* preview_data, const std::vector<std::string>& str_tool_colors)
{
    s_canvas_mgr.load_gcode_preview(canvas, preview_data, str_tool_colors);
}

void _3DScene::generate_legend_texture(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
{
    s_legend_texture.generate(preview_data, tool_colors);
}

unsigned int _3DScene::get_legend_texture_width()
{
    return s_legend_texture.get_texture_width();
}

unsigned int _3DScene::get_legend_texture_height()
{
    return s_legend_texture.get_texture_height();
}

void _3DScene::reset_legend_texture()
{
    s_legend_texture.reset_texture();
}

unsigned int _3DScene::finalize_legend_texture()
{
    return s_legend_texture.finalize();
}

unsigned int _3DScene::get_warning_texture_width()
{
    return s_warning_texture.get_texture_width();
}

unsigned int _3DScene::get_warning_texture_height()
{
    return s_warning_texture.get_texture_height();
}

void _3DScene::generate_warning_texture(const std::string& msg)
{
    s_warning_texture.generate(msg);
}

void _3DScene::reset_warning_texture()
{
    s_warning_texture.reset_texture();
}

unsigned int _3DScene::finalize_warning_texture()
{
    return s_warning_texture.finalize();
}

} // namespace Slic3r
