diff --git a/data/layout.xml b/data/layout.xml index 909800a..027bb8d 100644 --- a/data/layout.xml +++ b/data/layout.xml @@ -835,23 +835,23 @@ Here's a list of what's available in this release. - + - + - + - + - + @@ -875,7 +875,7 @@ Here's a list of what's available in this release. --> - + @@ -885,80 +885,97 @@ Here's a list of what's available in this release. - + - + - + - + - + - - - - - + + + + + + + + + + - + - + - + - - + + + + + + + + + + + + + - + - + - + diff --git a/src/app.h b/src/app.h index 226fd36..9120ad8 100644 --- a/src/app.h +++ b/src/app.h @@ -57,6 +57,7 @@ public: NodeMessageBox* msgbox; NodeSettings* settings; NodePopupMenu* popup = nullptr; + NodePopupMenu* subpopup = nullptr; NodePopupMenu* menu_file = nullptr; NodePopupMenu* menu_edit = nullptr; NodePopupMenu* menu_layers = nullptr; @@ -178,7 +179,8 @@ public: void dialog_save_ver(); void dialog_open(); void dialog_browse(); - void dialog_export(); + void dialog_export(std::string ext); + void dialog_export_layers(); void dialog_export_cubes(); void dialog_layer_rename(); void dialog_resize(); diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 43bf79d..4b95209 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -412,7 +412,7 @@ void App::dialog_save() } } -void App::dialog_export() +void App::dialog_export(std::string ext) { if (!check_license()) { @@ -423,7 +423,30 @@ void App::dialog_export() if (canvas) { // TODO: use picker - canvas->m_canvas->export_equirectangular(work_path + "/" + doc_name + ".jpg", [this]{ + canvas->m_canvas->export_equirectangular(work_path + "/" + doc_name + ext, [this]{ +#if defined(__IOS__) + message_box("Export JPG", "Image exported to Photos"); +#elif defined(__OSX__) + message_box("Export JPG", "Image exported to Pictures/PanoPainter folder"); +#elif defined(_WIN32) + message_box("Export JPG", "Image exported to " + work_path); +#endif + }); + } +} + +void App::dialog_export_layers() +{ + if (!check_license()) + { + message_box("License", "This function is disabled in demo mode."); + return; + } + + if (canvas) + { + // TODO: use picker + canvas->m_canvas->export_layers(doc_name, [this] { #if defined(__IOS__) message_box("Export JPG", "Image exported to Photos"); #elif defined(__OSX__) diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 7541fe1..fd45ed7 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -21,7 +21,7 @@ void App::init_toolbar_main() button->on_click = [this, button](Node*) { if (canvas) { - canvas->m_canvas->export_anim(); + //canvas->m_canvas->export_anim(); } }; } @@ -463,10 +463,40 @@ void App::init_menu_file() }; if (auto b = popup->find("file-export")) b->on_click = [this](Node*) { - dialog_export(); + dialog_export(".jpg"); popup->mouse_release(); popup->destroy(); }; + if (auto b = popup->find("file-export-tick")) + b->on_click = [this,b](Node*) { + glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0); + subpopup = (NodePopupMenu*)layout[const_hash("file-submenu-export")]->m_children[0]->clone(); + subpopup->update(); + if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) + pos.x = pos.x - subpopup->m_size.x + b->m_size.x; + subpopup->SetPositioning(YGPositionTypeAbsolute); + subpopup->SetPosition(pos.x, pos.y); + layout[main_id]->add_child(subpopup); + layout[main_id]->update(); + subpopup->mouse_capture(); + subpopup->m_mouse_ignore = false; + subpopup->m_flood_events = true; + subpopup->m_capture_children = false; + subpopup->find("file-submenu-export-png")->on_click = [this](Node*) { + dialog_export(".png"); + subpopup->mouse_release(); + subpopup->destroy(); + popup->mouse_release(); + popup->destroy(); + }; + subpopup->find("file-submenu-export-layers")->on_click = [this](Node*) { + dialog_export_layers(); + subpopup->mouse_release(); + subpopup->destroy(); + popup->mouse_release(); + popup->destroy(); + }; + }; if (auto b = popup->find("file-share")) b->on_click = [this](Node*) { share_file(doc_path); diff --git a/src/app_shaders.cpp b/src/app_shaders.cpp index 7861818..0c967d9 100644 --- a/src/app_shaders.cpp +++ b/src/app_shaders.cpp @@ -168,15 +168,15 @@ void App::initShaders() "void main() {\n" " mediump float stroke = 1.0 - texture(tex, uv).r;\n" " int zero_count = 0;\n" - " if (textureOffset(tex, uv, ivec2(-1, -1)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2(-1, 0)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2(-1, +1)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2( 0, -1)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2( 0, 0)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2( 0, +1)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2(+1, -1)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2(+1, 0)).r == 1.0) zero_count++;\n" - " if (textureOffset(tex, uv, ivec2(+1, +1)).r == 1.0) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2(-1, -1)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2(-1, 0)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2(-1, +1)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2( 0, -1)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2( 0, 0)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2( 0, +1)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2(+1, -1)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2(+1, 0)).r > 0.99) zero_count++;\n" + " if (textureOffset(tex, uv, ivec2(+1, +1)).r > 0.99) zero_count++;\n" " mediump float edge = (zero_count > 1 && zero_count < 9) ? 0.75 : 0.0;\n" " frag = vec4(col.rgb, edge * (1.0 - float(zero_count) / 9.f));\n" "}\n"; diff --git a/src/canvas.cpp b/src/canvas.cpp index 31378b6..18667d3 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -1334,7 +1334,7 @@ void Canvas::export_equirectangular_thread(std::string file_path) face.bind(); // copy the framebuffer before clearing to white glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height); - m_tmp[i].clear({ 1, 1, 1, 1 }); + m_tmp[i].clear({ 1, 1, 1, 0 }); m_plane.draw_fill(); face.unbind(); @@ -1402,10 +1402,10 @@ void Canvas::export_equirectangular_thread(std::string file_path) } LOG("writing %s", file_path.c_str()); - jpge::params params; - params.m_quality = 100; - bool saved = jpge::compress_image_to_jpeg_file(file_path.c_str(), m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), params); - inject_xmp(file_path.c_str()); + if (file_path.substr(file_path.size() - 4) == ".jpg") + stbi_write_jpg(file_path.c_str(), m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), 100); + else if (file_path.substr(file_path.size() - 4) == ".png") + stbi_write_png(file_path.c_str(), m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), 0); { progress++; @@ -1503,10 +1503,21 @@ void Canvas::inject_xmp(std::string jpg_path) } -void Canvas::export_anim() +void Canvas::export_layers(std::string file_name, std::function on_complete) +{ + if (App::I.check_license()) + { + std::thread t([=] { + export_layers_thread(file_name); + if (on_complete) + on_complete(); + }); + t.detach(); + } +} + +void Canvas::export_layers_thread(std::string file_name) { - if (!App::I.check_license()) - return; // save viewport and clear color states GLint vp[4]; GLfloat cc[4]; @@ -1514,6 +1525,24 @@ void Canvas::export_anim() glGetFloatv(GL_COLOR_CLEAR_VALUE, cc); GLboolean blend = glIsEnabled(GL_BLEND); + gl_state gl; + App::I.async_start(); + std::shared_ptr pb; + if (App::I.layout.m_loaded) + { + pb = std::make_shared(); + pb->m_manager = &App::I.layout; + pb->init(); + pb->create(); + pb->loaded(); + pb->m_progress->SetWidthP(0); + pb->m_title->set_text("Export Pano Layers"); + App::I.layout[App::I.main_id]->add_child(pb); + App::I.async_update(); + } + int progress = 0; + int total = (m_order.size() + 1) * 6; + // prepare common states glViewport(0, 0, m_width, m_height); @@ -1540,20 +1569,21 @@ void Canvas::export_anim() glViewport(0, 0, m_width, m_height); for (int i = 0; i < 6; i++) { + glEnable(GL_BLEND); m_tmp[i].bindFramebuffer(); - if (seq == 0) - { - m_tmp[i].clear({ 1, 1, 1, 1 }); - ShaderManager::use(kShader::Checkerboard); - ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); - m_plane.draw_fill(); - glEnable(GL_BLEND); - } - else + //if (seq == 0) + //{ + // m_tmp[i].clear({ 1, 1, 1, 1 }); + // ShaderManager::use(kShader::Checkerboard); + // ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); + // m_plane.draw_fill(); + // glEnable(GL_BLEND); + //} + //else { m_tmp[i].clear({ 1, 1, 1, 0 }); - glDisable(GL_BLEND); + //glDisable(GL_BLEND); } glActiveTexture(GL_TEXTURE0); @@ -1575,6 +1605,18 @@ void Canvas::export_anim() glBindTexture(GL_TEXTURE_CUBE_MAP, 0); m_tmp[i].unbindFramebuffer(); + + progress++; + float p = (float)progress / total * 100.f; + LOG("progress: %f", p); + + if (App::I.layout.m_loaded) + { + pb->m_progress->SetWidthP(p); + gl.save(); + App::I.async_update(); + gl.restore(); + } } @@ -1596,19 +1638,37 @@ void Canvas::export_anim() auto latlong_data = std::make_unique(m_latlong.bytes()); m_latlong.readTextureData(latlong_data.get()); static char name[128]; - sprintf(name, "%s/latlong-frame%02d.png", App::I.work_path.c_str(), seq); + sprintf(name, "%s/%s-layer-%02d.png", App::I.work_path.c_str(), file_name.c_str(), seq); seq++; LOG("writing %s", name); + App::I.async_end(); int ret = stbi_write_png(name, m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), m_latlong.stride()); - //jpge::params params; - //params.m_quality = 100; - //bool saved = jpge::compress_image_to_jpeg_file(name, m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), params); + App::I.async_start(); + } + + progress++; + float p = (float)progress / total * 100.f; + LOG("progress: %f", p); + + if (App::I.layout.m_loaded) + { + pb->m_progress->SetWidthP(p); + gl.save(); + App::I.async_update(); + gl.restore(); } } glDeleteTextures(1, &cube_id); m_latlong.destroy(); + if (App::I.layout.m_loaded) + { + pb->destroy(); + App::I.async_update(); + } + App::I.async_end(); + // restore viewport and clear color states blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND); glViewport(vp[0], vp[1], vp[2], vp[3]); diff --git a/src/canvas.h b/src/canvas.h index 096cbf6..e6bd564 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -242,7 +242,8 @@ public: void import_equirectangular_thread(std::string file_path); void export_equirectangular(std::string file_path, std::function on_complete = nullptr); void export_equirectangular_thread(std::string file_path); - void export_anim(); + void export_layers(std::string file_name, std::function on_complete = nullptr); + void export_layers_thread(std::string file_name); void export_cubes(); void project_save(std::function on_complete = nullptr); void project_save(std::string file_path, std::function on_complete = nullptr); diff --git a/src/image.cpp b/src/image.cpp index c917edc..298da79 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -59,6 +59,18 @@ void Image::flip() std::swap(m_data, flipped); } +void Image::gayscale_alpha() +{ + int np = width * height; + auto ptr = reinterpret_cast(m_data.get()); + for (int i = 0; i < np; i++) + { + auto& c = ptr[i]; + c.g = c.b = c.r = 255 - c.a; + c.a = 255; + } +} + Image Image::resize(int w, int h) const { Image ret; @@ -90,3 +102,50 @@ Image Image::resize(int w, int h) const } return ret; } + +Image Image::resize_power2() const +{ + int w = pow(2, ceil(log(width) / log(2))); + int h = pow(2, ceil(log(height) / log(2))); + if (w == width && h == height) + { + Image i; + i.create(width, height); + std::copy(m_data.get(), m_data.get() + width * height * comp, i.m_data.get()); + return i; + } + return resize(w, h); +} + +Image Image::resize_squared() const +{ + Image ret; + if (width == height) + { + ret.create(width, height); + std::copy(m_data.get(), m_data.get() + width * height * comp, ret.m_data.get()); + } + else + { + int pad_x = 0; + int pad_y = 0; + int size = 0; + if (height > width) + { + size = height; + pad_x = (size - width) / 2; + } + else + { + size = width; + pad_y = (size - height) / 2; + } + ret.create(size, size); + auto ptr_src = reinterpret_cast(m_data.get()); + auto ptr_dst = reinterpret_cast(ret.m_data.get()); + std::fill_n(ptr_dst, size * size, glm::u8vec4(0)); + for (int y = 0; y < height; y++) + std::copy_n(ptr_src + y * width, width, ptr_dst + pad_x + (y + pad_y) * ret.width); + } + return ret; +} diff --git a/src/image.h b/src/image.h index 263d3db..5aa8b00 100644 --- a/src/image.h +++ b/src/image.h @@ -31,6 +31,10 @@ public: m_data.reset(); } void flip(); + // convert to grayscale and set opaque + void gayscale_alpha(); void create() { m_data = std::make_unique(size()); } Image resize(int w, int h) const; + Image resize_power2() const; + Image resize_squared() const; }; diff --git a/src/node_panel_brush.cpp b/src/node_panel_brush.cpp index 6121116..ab9bbf3 100644 --- a/src/node_panel_brush.cpp +++ b/src/node_panel_brush.cpp @@ -65,9 +65,14 @@ void NodePanelBrush::init() std::string path_high = App::I.data_path + "/brushes/" + name + ".png"; std::string path_thumb = App::I.data_path + "/brushes/thumbs/" + name + ".png"; + + img = img.resize_squared(); + img.gayscale_alpha(); + auto thumb = img.resize(64, 64); thumb.save(path_thumb); - img.save(path_high); + auto po2 = img.resize_power2(); + po2.save(path_high); async_start(); NodeButtonBrush* brush = new NodeButtonBrush; @@ -75,7 +80,7 @@ void NodePanelBrush::init() brush->init(); brush->create(); brush->loaded(); - brush->set_icon(path.c_str()); + brush->set_icon(path_thumb.c_str()); brush->thumb_path = path_thumb; brush->high_path = path_high; brush->brush_name = name;