diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index 7bd456d8..1ea4e7e7 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -22,6 +22,7 @@ set(PP_LEGACY_PAINT_DOCUMENT_SOURCES src/canvas.cpp src/canvas_actions.cpp src/canvas_layer.cpp + src/legacy_canvas_document_io_services.cpp src/legacy_canvas_state_services.cpp src/event.cpp ) diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index e0cca580..7f008d54 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -79,13 +79,13 @@ What is still carrying too much live ownership: Current hotspot files: -- `src/canvas.cpp`: 3622 lines +- `src/canvas.cpp`: 2645 lines - `src/app_layout.cpp`: 1498 lines - `src/canvas_modes.cpp`: 1798 lines - `src/node.cpp`: 1551 lines - `src/main.cpp`: 1374 lines - `src/node_panel_brush.cpp`: 1197 lines -- `src/node_stroke_preview.cpp`: 1129 lines +- `src/node_stroke_preview.cpp`: 933 lines - `src/node_canvas.cpp`: 888 lines - `src/app.cpp`: 950 lines - `src/app_dialogs.cpp`: 908 lines @@ -155,8 +155,11 @@ Current architecture mismatches that must be treated as real blockers: canvas state-management cluster for picking, clear/clear-all, layer add/remove/order/lookups, animation frame control, resize, and snapshot save/restore now lives in `src/legacy_canvas_state_services.cpp` instead of - `src/canvas.cpp`, even though the bridge still owns worker-side readback flow - and encoder-state label reads. + `src/canvas.cpp`, while the larger import/export/save/open/thumbnail + document-IO cluster now lives in `src/legacy_canvas_document_io_services.cpp` + and `NodeStrokePreview` render-target setup plus immediate-pass sequencing + now route through retained preview execution helpers, even though the bridge + still owns worker-side readback flow and encoder-state label reads. - Modern C++23 usage exists in extracted components, especially `std::span`, explicit result/status objects, and a few concepts, but the live app still does not consistently express ownership, thread affinity, or renderer diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index d21c0b90..e4a3d56a 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -91,7 +91,7 @@ Status: In Progress Why now: `src/canvas.cpp` is still the biggest single architectural blocker at about -3622 lines. +2645 lines. Current slice: - Canvas state-management helpers for picking, clear/clear-all, layer @@ -99,6 +99,10 @@ Current slice: save/restore now live in `src/legacy_canvas_state_services.cpp` instead of staying inline in `src/canvas.cpp`, but the file still owns the larger document-plus-render shell. +- Canvas import/export/save/open/thumbnail ownership now lives in + `src/legacy_canvas_document_io_services.cpp` instead of staying inline in + `src/canvas.cpp`, which materially reduces document IO ownership in the live + render shell. Write scope: - `src/canvas.cpp` @@ -138,6 +142,12 @@ Current slice: - `NodeStrokePreview` final composite plus preview-texture copy now route through `legacy_node_stroke_preview_execution_services.h`, but the preview node still owns most live-pass and retained GL resource execution. +- `NodeStrokePreview` render-target setup plus immediate-pass sequence + orchestration now also route through + `legacy_node_stroke_preview_execution_services.h`, and duplicate render- + target setup was removed from `render_to_image()` and the queued worker path, + but the preview node still owns broader live-pass state and thread-facing + orchestration. - `NodeCanvas` merged-path per-plane merged-texture draw execution now also routes through `execute_legacy_canvas_draw_merge_layer_texture(...)`. - `NodeCanvas` merged-path and non-blend checkerboard background setup now also diff --git a/src/canvas.cpp b/src/canvas.cpp index feeabbc2..de93782d 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -2582,1103 +2582,6 @@ void Canvas::clear_context() } }; -void Canvas::import_equirectangular(std::string file_path, std::shared_ptr layer /*= nullptr*/) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), layer = std::move(layer)] { - BT_SetTerminate(); - import_equirectangular_thread(file_path, layer); - }); - } -} - -void Canvas::import_equirectangular_thread(std::string file_path, std::shared_ptr layer /*= nullptr*/, int frame /*= -1*/) -{ - Image img; - if (!img.load_file(file_path)) - return; - - if (!layer) - layer = m_layers[m_current_layer_idx]; - - if (frame == -1) - frame = layer->m_frame_index; - - auto a = new ActionImportEquirect; - a->m_layer = layer; - a->m_frame = frame; - a->m_snap = std::make_shared(layer->snapshot(frame)); - a->m_path = file_path; - ActionManager::add(a); - - m_unsaved = true; - - if (img.width == img.height / 6) - { - Texture2D tex; - static const GLint indices[] = { 5, 0, 4, 1, 2, 3 }; - const auto texture_format = texture_format_for_image_channels(img.comp); - tex.create( - img.width, - img.width, - static_cast(texture_format.internal_format), - static_cast(texture_format.pixel_format)); - int stride = img.width * img.width * img.comp; - Plane plane; - plane.create<1>(2, 2); - draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) { - apply_canvas_capability(depth_test_state(), false); - tex.update(img.m_data.get() + indices[i] * stride); - m_sampler.bind(0); - set_active_texture_unit(0); - tex.bind(); - pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader( - pp::panopainter::LegacyCanvasDrawMergeTextureUniforms { - .mvp = glm::scale(glm::vec3(-1, -1, 1)), - .texture_slot = 0, - }); - plane.draw_fill(); - tex.unbind(); - m_sampler.unbind(); - }, frame, false); - plane.destroy(); - } - else - { - Texture2D tex; - tex.load_file(file_path); - Sphere sphere; - sphere.create<64, 64>(2.f); - draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) { - apply_canvas_capability(depth_test_state(), false); - m_sampler.bind(0); - set_active_texture_unit(0); - tex.bind(); - pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader( - pp::panopainter::LegacyCanvasDrawMergeTextureUniforms { - .mvp = proj * camera * - glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1)), - .texture_slot = 0, - }); - sphere.draw_fill(); - tex.unbind(); - m_sampler.unbind(); - }, frame, false); - sphere.destroy(); - } - for (int i = 0; i < 6; i++) - { - layer->box(i) = glm::vec4(0, 0, m_width, m_height); - layer->face(i) = true; - } -} - -void Canvas::export_equirectangular(std::string file_path, std::function on_complete) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_equirectangular_thread(file_path); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } -} - -void Canvas::export_equirectangular_thread(std::string file_path) -{ - Image data; - - App::I->render_task([&] - { - draw_merge(false); - Texture2D equirect = m_layers_merge.gen_equirect(); - data = equirect.get_image(); - }); - - LOG("writing %s", file_path.c_str()); - if (file_path.substr(file_path.size() - 4) == ".jpg") - { - data.save_jpg(file_path, 100); - inject_xmp(file_path); - } - else if (file_path.substr(file_path.size() - 4) == ".png") - { - data.save_png(file_path); - } - - App::I->publish_exported_image(file_path); -} - -void Canvas::inject_xmp(std::string jpg_path) -{ - static const char xmp[] = -"http://ns.adobe.com/xap/1.0/\0" R"( - - - - equirectangular - True - 0 - 0 - 0 - 0 - 0 - PanoPainter - - - -)"; - - FILE* fp = fopen(jpg_path.c_str(), "rb"); - fseek(fp, 0, SEEK_END); - long len = ftell(fp); - fseek(fp, 0, SEEK_SET); - unsigned char* jpeg_data = (unsigned char*)malloc(len); - fread(jpeg_data, len, 1, fp); - fclose(fp); - - fp = fopen(jpg_path.c_str(), "wb"); - - int i = 0; - while (i < len && !(jpeg_data[i] == 0xff && jpeg_data[i + 1] == 0xd8)) i++; - i += 2; - - unsigned char* xmp_section = (unsigned char*)malloc(sizeof(xmp) + 4); - xmp_section[0] = 0xff; - xmp_section[1] = 0xe1; - xmp_section[2] = ((int)sizeof(xmp) + 2) >> 8; - xmp_section[3] = ((int)sizeof(xmp) + 2) >> 0; - memcpy(xmp_section + 4, xmp, sizeof(xmp)); - - fwrite(jpeg_data, 1, i, fp); - fwrite(xmp_section, 1, sizeof(xmp) + 4, fp); - fwrite(jpeg_data + i, 1, len - i, fp); - fclose(fp); - -} - -void Canvas::export_depth(std::string file_name, std::function on_complete) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_depth_thread(file_name); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } -} - -void Canvas::export_depth_thread(std::string file_name) -{ - RTT rtt; - rtt.create(1024, 1024); - - glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)rtt.getWidth() / (float)rtt.getHeight(), 0.1f, 100.f); - glm::mat4 camera = m_cam_rot; - - App::I->render_task([&] - { - draw_merge(false); - - rtt.bindFramebuffer(); - rtt.clear({ 0, 0, 0, 1 }); - apply_canvas_capability(blend_state(), true); - apply_canvas_capability(depth_test_state(), false); - apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight()); - for (int plane_index = 0; plane_index < 6; plane_index++) - { - auto plane_mvp_z = proj * camera * - m_plane_transform[plane_index] * - glm::translate(glm::vec3(0, 0, -1)) * - glm::scale(glm::vec3(2)); - - m_sampler.bind(0); - pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader( - pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms { - .mvp = plane_mvp_z, - .texture_slot = 0, - .alpha = 1.f, - .highlight = false, - }); - - set_active_texture_unit(0); - m_layers_merge.rtt(plane_index).bindTexture(); - m_plane.draw_fill(); - m_layers_merge.rtt(plane_index).unbindTexture(); - } - rtt.unbindFramebuffer(); - }); - - uint8_t* rgba_data = rtt.readTextureData(); - stbi_flip_vertically_on_write(true); - std::string path_rgba = App::I->work_path + "/" + file_name + ".png"; - stbi_write_jpg(path_rgba.c_str(), rtt.getWidth(), rtt.getHeight(), 4, rgba_data, 100); - delete rgba_data; - - App::I->render_task([&] - { - rtt.bindFramebuffer(); - rtt.clear({ 0, 0, 0, 1 }); - apply_canvas_capability(blend_state(), true); - apply_canvas_capability(depth_test_state(), false); - apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight()); - for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) - { - for (int plane_index = 0; plane_index < 6; plane_index++) - { - if ((!m_layers[layer_index]->m_visible || - m_layers[layer_index]->m_opacity == .0f || - !m_layers[layer_index]->face(plane_index))) - continue; - - auto plane_mvp_z = proj * camera * - m_plane_transform[plane_index] * - glm::translate(glm::vec3(0, 0, -1)) * - glm::scale(glm::vec3(2)); - - m_sampler.bind(0); - pp::panopainter::setup_legacy_canvas_draw_merge_texture_colorize_shader( - pp::panopainter::LegacyCanvasDrawMergeTextureColorizeUniforms { - .mvp = plane_mvp_z, - .texture_slot = 0, - .color = { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f }, - }); - - set_active_texture_unit(0); - m_layers[layer_index]->rtt(plane_index).bindTexture(); - m_plane.draw_fill(); - m_layers[layer_index]->rtt(plane_index).unbindTexture(); - } - } - rtt.unbindFramebuffer(); - }); - - uint8_t* depth_data = rtt.readTextureData(); - std::string path_depth = App::I->work_path + "/" + file_name + "_depth.png"; - stbi_write_jpg(path_depth.c_str(), rtt.getWidth(), rtt.getHeight(), 4, depth_data, 100); - delete depth_data; - stbi_flip_vertically_on_write(false); - - rtt.destroy(); -} - -void Canvas::export_layers(std::string path, std::function on_complete) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_layers_thread(path); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } -} - -void Canvas::export_layers_thread(std::string path) -{ - auto pb = App::I->show_progress("Export Layers", m_layers.size()); - for (int i = 0; i < m_layers.size(); i++) - { - auto l = m_layers[i]; - Image img = l->gen_equirect().get_image(); - img.save_png(fmt::format("{}-layer{:02d}-{}.png", path, i, l->m_name)); - pb->increment(); - } - pp::panopainter::close_legacy_dialog_node(*pb); -} - -void Canvas::export_anim_frames(std::string path, std::function on_complete) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_anim_frames_thread(path); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } -} - -void Canvas::export_anim_frames_thread(std::string path) -{ - auto pb = App::I->show_progress("Export Frames", anim_duration()); - for (int i = 0; i < anim_duration(); i++) - { - anim_goto_frame(i); - export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i)); - pb->increment(); - } - pp::panopainter::close_legacy_dialog_node(*pb); -} - -void Canvas::export_anim_mp4(std::string path, std::function on_complete) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_anim_mp4_thread(path); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } -} - -void Canvas::export_anim_mp4_thread(std::string path) -{ - auto pb = App::I->show_progress("Export Animation", anim_duration()); - int fps = App::I->animation->get_fps(); - MP4Encoder mp4; - int res = std::min(1024, m_width); - mp4.init(res * 4, res * 2, 30, 2 << 20); - for (int i = 0; i < anim_duration(); i++) - { - Image data; - App::I->render_task([&] - { - anim_goto_frame(i); - draw_merge(false); - Texture2D equirect = m_layers_merge.gen_equirect({ res, res }); - data = equirect.get_image(); - }); - for (int j = 0; j < 30/fps; j++) - mp4.encode(data); - pb->increment(); - } - mp4.write_mp4(path); - pp::panopainter::close_legacy_dialog_node(*pb); -} - -void Canvas::export_cube_faces(std::string file_name, std::function on_complete) -{ - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_cube_faces_thread(file_name); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } -} - -void Canvas::export_cube_faces_thread(std::string file_name) -{ -#ifdef __OBJC__ - NSMutableArray* files = [NSMutableArray array]; -#endif - static std::array plane_names{ "front", "right", "back", "left", "top", "bottom" }; - - auto pb = App::I->show_progress("Export Cube Faces", 7); - - App::I->render_task([this] { - draw_merge(false); - }); - pb->increment(); - - for (int i = 0; i < 6; i++) - { - Image face = m_layers_merge.rtt(i).get_image(); - std::string path = fmt::format("{}/{}-{}.png", App::I->work_path, file_name, plane_names[i]); - face.save_png(path); - pb->increment(); - - App::I->publish_exported_image(path); -#ifdef __OBJC__ - [files addObject : [NSString stringWithUTF8String:path.c_str()] ]; -#endif - } - - pp::panopainter::close_legacy_dialog_node(*pb); - -#ifdef __OBJC__ - static char name[128]; - sprintf(name, "%s.zip", App::I->work_path.c_str()); - auto zip_path = [NSString stringWithUTF8String:name]; - //[SSZipArchive createZipFileAtPath:zip_path withFilesAtPaths:files]; - //for (NSString* f : files) - // [[NSFileManager defaultManager] removeItemAtPath:f error:nil]; -#endif -} - -void Canvas::project_save(std::function on_complete) -{ - if (App::I->check_license()) - { - const auto file_path = App::I->doc_path; - App::I->runtime().canvas_async_task([this, file_path, on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - bool ret = project_save_thread(file_path, true); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); }); - }); - } -} - -void Canvas::project_save(std::string file_path, std::function on_complete) -{ - LOG("saving %s", file_path.c_str()); - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - bool ret = project_save_thread(file_path, true); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); }); - }); - } - else - { - LOG("no license, no save"); - } -} - -bool Canvas::project_save_thread(std::string file_path, bool show_progress) -{ - // already saved, nothing to do - if (!m_unsaved && file_path == App::I->doc_path) - { - LOG("already saved"); - return true; - } - -// static char name[128]; -// sprintf(name, "%s/latlong.ppi", data_path.c_str()); - FILE* fp = nullptr; - - const auto save_target = pp::app::plan_document_canvas_project_save_target(App::I->data_path, file_path); - if (!save_target) { - LOG("cannot plan project save target for %s: %s", file_path.c_str(), save_target.status().message); - return false; - } - const auto& save_paths = save_target.value(); - const std::string& file_name = save_paths.file_name; - const std::string& tmp_path = save_paths.temporary_path; - const std::string& lapse_path = save_paths.timelapse_path; - - LOG("file name %s", file_name.c_str()); - LOG("tmp path %s", tmp_path.c_str()); - - bool target_exists = false; - if ((fp = fopen(file_path.c_str(), "rb"))) { - fclose(fp); - fp = nullptr; - target_exists = true; - } - - const auto write_plan = pp::app::plan_document_canvas_project_save_write(save_paths, target_exists); - if (!write_plan) { - LOG("cannot plan project save write for %s: %s", file_path.c_str(), write_plan.status().message); - return false; - } - - bool use_tmp = write_plan.value().uses_temporary; - if (write_plan.value().uses_temporary) - { - LOG("use tmp file"); - fp = fopen(write_plan.value().write_path.c_str(), "wb"); - if (!fp) - { - LOG("cannot write tmp project to %s", tmp_path.c_str()); - use_tmp = false; - } - } - - LOG("save first time"); - - if (!fp) - { - // write directly to the new file - if (!(fp = fopen(file_path.c_str(), "wb"))) - { - LOG("cannot write project to %s", file_path.c_str()); - return false; - } - LOG("unsafe mode saving directly to %s", file_path.c_str()); - } - - PPIHeader ppi_header; - fwrite(&ppi_header, sizeof(PPIHeader), 1, fp); - - // load thumbnail - Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height); - - std::shared_ptr pb; - if (show_progress) - pb = App::I->show_progress("Saving Pano Project"); - - thumb.flip(); - fwrite(thumb.data(), thumb.size(), 1, fp); - - fwrite(&m_width, sizeof(int), 1, fp); - fwrite(&m_height, sizeof(int), 1, fp); - - int n_layers = (int)m_layers.size(); - fwrite(&n_layers, sizeof(int), 1, fp); - - int n_frames = std::accumulate(m_layers.begin(), m_layers.end(), 0, - [](int tot, auto& l) { return tot + l->frames_count(); }); - if (ppi_header.doc_version.minor >= 3) - fwrite(&n_frames, sizeof(int), 1, fp); - - int progress = 0; - int total = n_frames * 6; - - for (int i = 0; i < (int)m_layers.size(); i++) - { - int n_order = i; - fwrite(&n_order, sizeof(int), 1, fp); - - float layer_alpha = m_layers[i]->m_opacity; - fwrite(&layer_alpha, sizeof(float), 1, fp); - - int name_len = (int)m_layers[i]->m_name.size(); - fwrite(&name_len, sizeof(int), 1, fp); - fwrite(m_layers[i]->m_name.data(), name_len, 1, fp); - - if (ppi_header.doc_version.minor >= 2) - { - fwrite(&m_layers[i]->m_blend_mode, sizeof(int), 1, fp); - fwrite(&m_layers[i]->m_alpha_locked, sizeof(bool), 1, fp); - fwrite(&m_layers[i]->m_visible, sizeof(bool), 1, fp); - } - - int frames = 1; - if (ppi_header.doc_version.minor >= 3) - { - frames = (int)m_layers[i]->frames_count(); - fwrite(&frames, sizeof(int), 1, fp); - } - - for (int fi = 0; fi < frames; fi++) - { - if (ppi_header.doc_version.minor >= 3) - { - int duration = m_layers[i]->frame_duration(fi); - fwrite(&duration, sizeof(int), 1, fp); - } - bool gpu = m_layers[i]->frame(fi).gpu_load(); - m_layers[i]->optimize(fi); - auto snap = m_layers[i]->snapshot(fi); - for (int plane_index = 0; plane_index < 6; plane_index++) - { - int has_data = snap.m_dirty_face[plane_index] ? 1 : 0; - fwrite(&has_data, sizeof(int), 1, fp); - if (has_data) - { - glm::ivec4 b = snap.m_dirty_box[plane_index]; - glm::vec2 sz = zw(b) - xy(b); - int box[4] = { b.x, b.y, b.z, b.w }; - fwrite(&box, sizeof(box), 1, fp); - - std::vector compressed; - auto callback = [](void* context, void* data, int size) - { - std::vector* buffer = static_cast*>(context); - buffer->insert(buffer->end(), (uint8_t*)data, (uint8_t*)data + size); - }; - int ret = stbi_write_png_to_func(callback, &compressed, sz.x, sz.y, 4, snap.image[plane_index].get(), sz.x * 4); - - int data_size = (int)compressed.size(); - fwrite(&data_size, sizeof(int), 1, fp); - - fwrite(compressed.data(), 1, compressed.size(), fp); - } - progress++; - float p = (float)progress / total * 100.f; - if (show_progress) - pb->m_progress->SetWidthP(p); - LOG("progress: %f", p); - } - if (!gpu) - m_layers[i]->frame(fi).gpu_unload(); - } - } - - if (ppi_header.doc_version.minor >= 4) - { - BinaryStreamWriter sw; - sw.init(BinaryStream::ByteOrder::LittleEndian); - - Serializer::Descriptor info; - info.class_id = "ppi_info"; - info.name = L"info header"; - //info.props["has_encoder"] = std::make_shared(m_encoder != nullptr); - sw << info; - //if (m_encoder != nullptr) - // sw << *m_encoder; - - int bytes = sw.m_data.size(); - fwrite(&bytes, sizeof(int), 1, fp); - fwrite((char*)sw.m_data.data(), sw.m_data.size(), 1, fp); - } - - fclose(fp); - - bool target_remove_attempted = false; - bool target_remove_succeeded = false; - bool temporary_rename_attempted = false; - bool temporary_rename_succeeded = false; - if (use_tmp) - { - LOG("project saved tmp to %s", tmp_path.c_str()); - LOG("swapping to %s", file_path.c_str()); - target_remove_attempted = true; - target_remove_succeeded = std::remove(file_path.c_str()) == 0; - if (target_remove_succeeded) - { - temporary_rename_attempted = true; - temporary_rename_succeeded = std::rename(tmp_path.c_str(), file_path.c_str()) == 0; - } - } - - const auto commit_plan = pp::app::plan_document_canvas_project_save_commit( - pp::app::DocumentCanvasProjectSaveCommitInput { - .used_temporary = use_tmp, - .target_remove_attempted = target_remove_attempted, - .target_remove_succeeded = target_remove_succeeded, - .temporary_rename_attempted = temporary_rename_attempted, - .temporary_rename_succeeded = temporary_rename_succeeded, - }); - const bool success = commit_plan.saved; - if (commit_plan.saved && commit_plan.temporary_renamed) { - LOG("tmp file swapped succesfully"); - } else if (!commit_plan.saved && commit_plan.target_may_be_missing) { - LOG("tmp file NOT swapped, original removed"); - } else if (!commit_plan.saved && commit_plan.used_temporary) { - LOG("could not remove %s", file_path.c_str()); - } else if (commit_plan.saved) { - LOG("project saved to %s", file_path.c_str()); - } - - const auto post_commit_plan = pp::app::plan_document_canvas_project_save_post_commit( - pp::app::DocumentCanvasProjectSavePostCommitInput { - .save_succeeded = success, - .timelapse_encoder_available = Canvas::I->m_encoder != nullptr, - .progress_ui_visible = show_progress, - }); - - if (post_commit_plan.marks_document_clean) - { - m_unsaved = false; - } - if (post_commit_plan.marks_new_document_committed) - { - m_newdoc = false; - } - - if (post_commit_plan.saves_timelapse_sidecar) - { - BinaryStreamWriter sw; - sw.init(BinaryStream::ByteOrder::LittleEndian); - Serializer::Descriptor info; - info.class_id = "tracks-info"; - info.name = L"Timelapse Tracks"; - info.props["has-track-360"] = std::make_shared(true); - info.props["version"] = std::make_shared(1); - sw << info; - sw << *Canvas::I->m_encoder; - if (!sw.save(lapse_path)) - LOG("cannot save timelase to %s", lapse_path.c_str()); - } - - if (post_commit_plan.flushes_platform_storage) - { - App::I->flush_platform_storage(); - } - - if (post_commit_plan.dismisses_progress_ui) - { - pp::panopainter::close_legacy_dialog_node(*pb); - } - if (post_commit_plan.updates_title) - { - App::I->title_update(); - } - - return success; -} - -void Canvas::project_open(std::string file_path, std::function on_complete) -{ - App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - bool result = project_open_thread(file_path); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete), result]() mutable { on_complete(result); }); - }); -} - -bool Canvas::project_open_thread(std::string file_path) -{ - FILE* fp = fopen(file_path.c_str(), "rb"); - if (!fp) - { - LOG("cannot write project to %s", file_path.c_str()); - return false; // should probably return a bool - } - - PPIHeader ppi_header; - fread(&ppi_header, sizeof(PPIHeader), 1, fp); - - if (!ppi_header.valid()) - { - LOG("INVALID PPI HEADER"); - return false; - } - - std::shared_ptr pb; - if (App::I->layout.m_loaded) - { - pb = std::make_shared(); - pb->set_manager(&App::I->layout); - pb->init(); - pb->create(); - pb->loaded(); - pb->m_progress->SetWidthP(0); - pb->m_title->set_text("Opening Pano Project"); - App::I->layout[App::I->main_id]->add_child(pb); - } - - // skip thumbnail - Image thumb; - thumb.width = ppi_header.thumb_header.width; - thumb.height = ppi_header.thumb_header.height; - thumb.comp = ppi_header.thumb_header.comp; - fseek(fp, thumb.size(), SEEK_CUR); - - fread(&m_width, sizeof(int), 1, fp); - fread(&m_height, sizeof(int), 1, fp); - - int n_layers = 0; - fread(&n_layers, sizeof(int), 1, fp); - int n_frames = 1; - if (ppi_header.doc_version.minor >= 3) - fread(&n_frames, sizeof(int), 1, fp); - - const int bytes = m_width * m_height * 4; - LayerFrame::Snapshot snap; - snap.create(m_width, m_height); // allocate single data, no box should be bigger - - int progress = 0; - int total = n_frames * 6; - - for (auto& l : m_layers) - l->destroy(); - m_layers.clear(); - //clear_all(); - resize(m_width, m_height); - - std::vector> tmp_layers(n_layers); - - for (int i = 0; i < n_layers; i++) - { - int n_order; - fread(&n_order, sizeof(int), 1, fp); - - //if (ppi_header.doc_version.minor > 1) - // n_order = i; - - tmp_layers[n_order] = std::make_unique(); - auto& layer = tmp_layers[n_order]; - - fread(&layer->m_opacity, sizeof(float), 1, fp); - - int name_len; - fread(&name_len, sizeof(int), 1, fp); - std::string name(name_len, '\0'); - fread((char*)name.data(), name_len, 1, fp); - - if (ppi_header.doc_version.minor >= 2) - { - fread(&layer->m_blend_mode, sizeof(int), 1, fp); - fread(&layer->m_alpha_locked, sizeof(bool), 1, fp); - fread(&layer->m_visible, sizeof(bool), 1, fp); - } - - int frames = 1; - if (ppi_header.doc_version.minor >= 3) - fread(&frames, sizeof(int), 1, fp); - - layer->create(m_width, m_height, name.c_str()); - - for (int fi = 0; fi < frames; fi++) - { - if (fi > 0) - layer->add_frame(); - if (ppi_header.doc_version.minor >= 3) - { - int duration = layer->frame_duration(fi); - fread(&duration, sizeof(int), 1, fp); - } - snap.clear(); - for (int plane_index = 0; plane_index < 6; plane_index++) - { - int has_data; - fread(&has_data, sizeof(int), 1, fp); - snap.m_dirty_face[plane_index] = has_data; - if (has_data) - { - int b[4]; - fread(&b, sizeof(b), 1, fp); - snap.m_dirty_box[plane_index] = glm::vec4(b[0], b[1], b[2], b[3]); - glm::vec2 sz = zw(snap.m_dirty_box[plane_index]) - xy(snap.m_dirty_box[plane_index]); - - int data_size; - fread(&data_size, sizeof(int), 1, fp); - std::vector compressed(data_size); - - fread(compressed.data(), 1, data_size, fp); - int imgw, imgh, imgc; - uint8_t* rgba = stbi_load_from_memory(compressed.data(), data_size, &imgw, &imgh, &imgc, 4); - if (rgba) - { - std::copy(rgba, rgba + (imgw * imgh * 4), snap.image[plane_index].get()); - delete rgba; - } - } - - progress++; - float p = (float)progress / total * 100.f; - LOG("progress: %f", p); - - if (App::I->layout.m_loaded) - { - pb->m_progress->SetWidthP(p); - } - } - layer->restore(snap, fi); - } - } - - std::swap(tmp_layers, m_layers); - - if (ppi_header.doc_version.minor >= 4) - { - int bytes = 0; - fread(&bytes, sizeof(int), 1, fp); - std::vector data(bytes); - fread(data.data(), bytes, 1, fp); - BinaryStreamReader sr; - sr.init(data.data(), data.size(), BinaryStream::ByteOrder::LittleEndian); - Serializer::Descriptor info; - sr >> info; - //if (info.value("has_encoder")) - //{ - // m_encoder = std::make_unique(); - // sr >> *m_encoder; - // m_encoder->init(); - //} - //else - //{ - // timelapse_reset_encoder(); - //} - } - //else - //{ - // timelapse_reset_encoder(); - //} - - fclose(fp); - LOG("project restore from %s", file_path.c_str()); - - auto start = file_path.rfind('/') + 1; - std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi")); - std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl"; - if (Asset::exist(lapse_path)) - { - BinaryStreamReader sr; - sr.load(lapse_path, BinaryStream::ByteOrder::LittleEndian); - Serializer::Descriptor info; - sr >> info; - if (info.value("has-track-360")) - { - m_encoder = std::make_unique(); - sr >> *m_encoder; - m_encoder->init(); - } - } - else - { - timelapse_reset_encoder(); - } - - m_current_layer_idx = 0; - m_current_stroke = nullptr; - m_dual_stroke = nullptr; - m_show_tmp = false; - m_smask_active = false; - m_smask_mode = 0; - m_dirty = false; - m_commit_delayed = false; - m_dirty_stroke = false; - memset(m_dirty_face, 0, sizeof(bool) * 6); - memset(m_pick_ready, 0, sizeof(bool) * 6); - m_unsaved = false; - m_newdoc = false; - if (App::I->layout.m_loaded) - { - pp::panopainter::close_legacy_dialog_node(*pb); - App::I->ui_task([] { - App::I->title_update(); - App::I->update_rec_frames(); - Canvas::I->anim_update(); - App::I->animation->load_layers(); - }); - } - return true; -} - -Image Canvas::thumbnail_generate(int w, int h) -{ - Image image; - image.create(w, h); - - App::I->render_task([this, w, h, &image] - { - // save viewport and clear color states - const auto vp = query_canvas_viewport(); - const auto cc = query_canvas_clear_color(); - auto blend = query_canvas_capability(blend_state()); - - // prepare common states - apply_canvas_viewport(0, 0, w, h); - - RTT fb; - fb.create(w, h); - fb.bindFramebuffer(); - Plane m_face_plane; - m_face_plane.create<1>(2, 2); - Texture2D blendtex; - blendtex.create(w, h); - const auto layer_feedback = canvas_destination_feedback_plan(w, h); - const bool copy_layer_destination = !layer_feedback.reads_destination_color; - - // recalculate because of different aspect ratio than the m_proj matrix - glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f); - - fb.clear({ 1, 1, 1, 0 }); - for (int i = 0; i < 6; i++) - { - apply_canvas_capability(blend_state(), false); - auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1)); - - if (copy_layer_destination) - { - set_active_texture_unit(2); - blendtex.bind(); - m_sampler_nearest.bind(2); - } - m_sampler_nearest.bind(0); // nearest - for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) - { - if (!m_layers[layer_index]->m_visible || - m_layers[layer_index]->m_opacity == 0.f || - !m_layers[layer_index]->face(i)) - continue; - if (copy_layer_destination) - { - set_active_texture_unit(2); - copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h); - } - pp::panopainter::setup_legacy_canvas_draw_merge_texture_blend_shader( - pp::panopainter::LegacyCanvasDrawMergeTextureBlendUniforms { - .mvp = plane_mvp, - .texture_slot = 0, - .destination_texture_slot = 2, - .use_destination_texture = copy_layer_destination, - .blend_mode = m_layers[layer_index]->m_blend_mode, - .alpha = m_layers[layer_index]->m_opacity, - }); - set_active_texture_unit(0); - m_layers[layer_index]->rtt(i).bindTexture(); - m_face_plane.draw_fill(); - m_layers[layer_index]->rtt(i).unbindTexture(); - } - - if (copy_layer_destination) - { - set_active_texture_unit(2); - blendtex.unbind(); - } - - set_active_texture_unit(0); - blendtex.bind(); - // copy the content of the fb before drawing the grid - copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h); - - // draw the grid - pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader( - pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms { - .mvp = plane_mvp, - }); - m_face_plane.draw_fill(); - - // now blend with the background - apply_canvas_capability(blend_state(), true); - pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader( - pp::panopainter::LegacyCanvasDrawMergeTextureUniforms { - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), - .texture_slot = 0, - }); - m_sampler.bind(0); // linear - m_plane.draw_fill(); - - blendtex.unbind(); - } - - fb.unbindFramebuffer(); - - // read the rendered image - fb.readTextureData((uint8_t*)image.data()); - - fb.destroy(); - blendtex.destroy(); - - // restore viewport and clear color states - blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false); - apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height); - apply_canvas_clear_color(cc); - set_active_texture_unit(0); - }); - - return image; -} - -Image Canvas::thumbnail_read(std::string file_path) -{ -// static char name[128]; -// sprintf(name, "%s/latlong.ppi", data_path.c_str()); - FILE* fp = fopen(file_path.c_str(), "rb"); - if (!fp) - { - LOG("cannot read project %s", file_path.c_str()); - return {}; // return empty image - } - PPIHeader ppi_header; - fread(&ppi_header, sizeof(PPIHeader), 1, fp); - - if (!ppi_header.valid()) - return {}; - - Image thumb; - thumb.width = ppi_header.thumb_header.width; - thumb.height = ppi_header.thumb_header.height; - thumb.comp = ppi_header.thumb_header.comp; - thumb.create(); - fread((uint8_t*)thumb.data(), thumb.size(), 1, fp); - fclose(fp); - LOG("project thumbnail read from %s", file_path.c_str()); - return thumb; -} - void Canvas::draw_objects_direct(std::function observer, Layer& layer, int frame) { App::I->render_task([&] diff --git a/src/legacy_canvas_document_io_services.cpp b/src/legacy_canvas_document_io_services.cpp new file mode 100644 index 00000000..248fec00 --- /dev/null +++ b/src/legacy_canvas_document_io_services.cpp @@ -0,0 +1,1195 @@ +#include "pch.h" + +#include "log.h" +#include "canvas.h" +#include "app.h" +#include "legacy_canvas_draw_merge_services.h" +#include "legacy_ui_gl_dispatch.h" +#include "legacy_ui_overlay_services.h" +#include "app_core/document_canvas.h" +#include "texture.h" +#include "node_progress_bar.h" +#include "paint_renderer/compositor.h" +#include "renderer_gl/opengl_capabilities.h" +#include "util.h" +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +namespace { + +GLenum depth_test_state() +{ + return static_cast(pp::renderer::gl::depth_test_state()); +} + +GLenum blend_state() +{ + return static_cast(pp::renderer::gl::blend_state()); +} + +pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count) +{ + return pp::renderer::gl::texture_format_for_channel_count(static_cast(channel_count)); +} + +void set_active_texture_unit(std::uint32_t unit_index) +{ + pp::legacy::ui_gl::activate_texture_unit(unit_index, "Canvas"); +} + +void apply_canvas_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) +{ + pp::legacy::ui_gl::apply_viewport(x, y, width, height, "Canvas"); +} + +pp::renderer::gl::OpenGlViewportRect query_canvas_viewport() +{ + return pp::legacy::ui_gl::query_viewport_rect("Canvas"); +} + +std::array query_canvas_clear_color() +{ + return pp::legacy::ui_gl::query_clear_color("Canvas"); +} + +void apply_canvas_clear_color(std::array color) +{ + pp::legacy::ui_gl::set_clear_color(color, "Canvas"); +} + +void apply_canvas_capability(std::uint32_t state, bool enabled) +{ + pp::legacy::ui_gl::set_capability(state, enabled, "Canvas"); +} + +bool query_canvas_capability(std::uint32_t state) +{ + return pp::legacy::ui_gl::query_capability(state, "Canvas"); +} + +pp::renderer::RenderDeviceFeatures canvas_render_device_features() noexcept +{ + return ShaderManager::render_device_features(); +} + +pp::paint_renderer::CanvasStrokeRasterizationPlan canvas_stroke_rasterization_plan( + int width, + int height) noexcept +{ + return pp::panopainter::plan_legacy_canvas_stroke_rasterization( + canvas_render_device_features(), + width, + height); +} + +pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_destination_feedback_plan( + int width, + int height) noexcept +{ + return canvas_stroke_rasterization_plan(width, height).feedback; +} + +} // namespace + +void Canvas::import_equirectangular(std::string file_path, std::shared_ptr layer /*= nullptr*/) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), layer = std::move(layer)] { + BT_SetTerminate(); + import_equirectangular_thread(file_path, layer); + }); + } +} + +void Canvas::import_equirectangular_thread(std::string file_path, std::shared_ptr layer /*= nullptr*/, int frame /*= -1*/) +{ + Image img; + if (!img.load_file(file_path)) + return; + + if (!layer) + layer = m_layers[m_current_layer_idx]; + + if (frame == -1) + frame = layer->m_frame_index; + + auto a = new ActionImportEquirect; + a->m_layer = layer; + a->m_frame = frame; + a->m_snap = std::make_shared(layer->snapshot(frame)); + a->m_path = file_path; + ActionManager::add(a); + + m_unsaved = true; + + if (img.width == img.height / 6) + { + Texture2D tex; + static const GLint indices[] = { 5, 0, 4, 1, 2, 3 }; + const auto texture_format = texture_format_for_image_channels(img.comp); + tex.create( + img.width, + img.width, + static_cast(texture_format.internal_format), + static_cast(texture_format.pixel_format)); + int stride = img.width * img.width * img.comp; + Plane plane; + plane.create<1>(2, 2); + draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) { + apply_canvas_capability(depth_test_state(), false); + tex.update(img.m_data.get() + indices[i] * stride); + m_sampler.bind(0); + set_active_texture_unit(0); + tex.bind(); + pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader( + pp::panopainter::LegacyCanvasDrawMergeTextureUniforms { + .mvp = glm::scale(glm::vec3(-1, -1, 1)), + .texture_slot = 0, + }); + plane.draw_fill(); + tex.unbind(); + m_sampler.unbind(); + }, frame, false); + plane.destroy(); + } + else + { + Texture2D tex; + tex.load_file(file_path); + Sphere sphere; + sphere.create<64, 64>(2.f); + draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) { + apply_canvas_capability(depth_test_state(), false); + m_sampler.bind(0); + set_active_texture_unit(0); + tex.bind(); + pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader( + pp::panopainter::LegacyCanvasDrawMergeTextureUniforms { + .mvp = proj * camera * + glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1)), + .texture_slot = 0, + }); + sphere.draw_fill(); + tex.unbind(); + m_sampler.unbind(); + }, frame, false); + sphere.destroy(); + } + for (int i = 0; i < 6; i++) + { + layer->box(i) = glm::vec4(0, 0, m_width, m_height); + layer->face(i) = true; + } +} + +void Canvas::export_equirectangular(std::string file_path, std::function on_complete) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + export_equirectangular_thread(file_path); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); + }); + } +} + +void Canvas::export_equirectangular_thread(std::string file_path) +{ + Image data; + + App::I->render_task([&] + { + draw_merge(false); + Texture2D equirect = m_layers_merge.gen_equirect(); + data = equirect.get_image(); + }); + + LOG("writing %s", file_path.c_str()); + if (file_path.substr(file_path.size() - 4) == ".jpg") + { + data.save_jpg(file_path, 100); + inject_xmp(file_path); + } + else if (file_path.substr(file_path.size() - 4) == ".png") + { + data.save_png(file_path); + } + + App::I->publish_exported_image(file_path); +} + +void Canvas::inject_xmp(std::string jpg_path) +{ + static const char xmp[] = +"http://ns.adobe.com/xap/1.0/\0" R"( + + + + equirectangular + True + 0 + 0 + 0 + 0 + 0 + PanoPainter + + + +)"; + + FILE* fp = fopen(jpg_path.c_str(), "rb"); + fseek(fp, 0, SEEK_END); + long len = ftell(fp); + fseek(fp, 0, SEEK_SET); + unsigned char* jpeg_data = (unsigned char*)malloc(len); + fread(jpeg_data, len, 1, fp); + fclose(fp); + + fp = fopen(jpg_path.c_str(), "wb"); + + int i = 0; + while (i < len && !(jpeg_data[i] == 0xff && jpeg_data[i + 1] == 0xd8)) i++; + i += 2; + + unsigned char* xmp_section = (unsigned char*)malloc(sizeof(xmp) + 4); + xmp_section[0] = 0xff; + xmp_section[1] = 0xe1; + xmp_section[2] = ((int)sizeof(xmp) + 2) >> 8; + xmp_section[3] = ((int)sizeof(xmp) + 2) >> 0; + memcpy(xmp_section + 4, xmp, sizeof(xmp)); + + fwrite(jpeg_data, 1, i, fp); + fwrite(xmp_section, 1, sizeof(xmp) + 4, fp); + fwrite(jpeg_data + i, 1, len - i, fp); + fclose(fp); + +} + +void Canvas::export_depth(std::string file_name, std::function on_complete) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + export_depth_thread(file_name); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); + }); + } +} + +void Canvas::export_depth_thread(std::string file_name) +{ + RTT rtt; + rtt.create(1024, 1024); + + glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)rtt.getWidth() / (float)rtt.getHeight(), 0.1f, 100.f); + glm::mat4 camera = m_cam_rot; + + App::I->render_task([&] + { + draw_merge(false); + + rtt.bindFramebuffer(); + rtt.clear({ 0, 0, 0, 1 }); + apply_canvas_capability(blend_state(), true); + apply_canvas_capability(depth_test_state(), false); + apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight()); + for (int plane_index = 0; plane_index < 6; plane_index++) + { + auto plane_mvp_z = proj * camera * + m_plane_transform[plane_index] * + glm::translate(glm::vec3(0, 0, -1)) * + glm::scale(glm::vec3(2)); + + m_sampler.bind(0); + pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader( + pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms { + .mvp = plane_mvp_z, + .texture_slot = 0, + .alpha = 1.f, + .highlight = false, + }); + + set_active_texture_unit(0); + m_layers_merge.rtt(plane_index).bindTexture(); + m_plane.draw_fill(); + m_layers_merge.rtt(plane_index).unbindTexture(); + } + rtt.unbindFramebuffer(); + }); + + uint8_t* rgba_data = rtt.readTextureData(); + stbi_flip_vertically_on_write(true); + std::string path_rgba = App::I->work_path + "/" + file_name + ".png"; + stbi_write_jpg(path_rgba.c_str(), rtt.getWidth(), rtt.getHeight(), 4, rgba_data, 100); + delete rgba_data; + + App::I->render_task([&] + { + rtt.bindFramebuffer(); + rtt.clear({ 0, 0, 0, 1 }); + apply_canvas_capability(blend_state(), true); + apply_canvas_capability(depth_test_state(), false); + apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight()); + for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) + { + for (int plane_index = 0; plane_index < 6; plane_index++) + { + if ((!m_layers[layer_index]->m_visible || + m_layers[layer_index]->m_opacity == .0f || + !m_layers[layer_index]->face(plane_index))) + continue; + + auto plane_mvp_z = proj * camera * + m_plane_transform[plane_index] * + glm::translate(glm::vec3(0, 0, -1)) * + glm::scale(glm::vec3(2)); + + m_sampler.bind(0); + pp::panopainter::setup_legacy_canvas_draw_merge_texture_colorize_shader( + pp::panopainter::LegacyCanvasDrawMergeTextureColorizeUniforms { + .mvp = plane_mvp_z, + .texture_slot = 0, + .color = { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f }, + }); + + set_active_texture_unit(0); + m_layers[layer_index]->rtt(plane_index).bindTexture(); + m_plane.draw_fill(); + m_layers[layer_index]->rtt(plane_index).unbindTexture(); + } + } + rtt.unbindFramebuffer(); + }); + + uint8_t* depth_data = rtt.readTextureData(); + std::string path_depth = App::I->work_path + "/" + file_name + "_depth.png"; + stbi_write_jpg(path_depth.c_str(), rtt.getWidth(), rtt.getHeight(), 4, depth_data, 100); + delete depth_data; + stbi_flip_vertically_on_write(false); + + rtt.destroy(); +} + +void Canvas::export_layers(std::string path, std::function on_complete) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + export_layers_thread(path); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); + }); + } +} + +void Canvas::export_layers_thread(std::string path) +{ + auto pb = App::I->show_progress("Export Layers", m_layers.size()); + for (int i = 0; i < m_layers.size(); i++) + { + auto l = m_layers[i]; + Image img = l->gen_equirect().get_image(); + img.save_png(fmt::format("{}-layer{:02d}-{}.png", path, i, l->m_name)); + pb->increment(); + } + pp::panopainter::close_legacy_dialog_node(*pb); +} + +void Canvas::export_anim_frames(std::string path, std::function on_complete) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + export_anim_frames_thread(path); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); + }); + } +} + +void Canvas::export_anim_frames_thread(std::string path) +{ + auto pb = App::I->show_progress("Export Frames", anim_duration()); + for (int i = 0; i < anim_duration(); i++) + { + anim_goto_frame(i); + export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i)); + pb->increment(); + } + pp::panopainter::close_legacy_dialog_node(*pb); +} + +void Canvas::export_anim_mp4(std::string path, std::function on_complete) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + export_anim_mp4_thread(path); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); + }); + } +} + +void Canvas::export_anim_mp4_thread(std::string path) +{ + auto pb = App::I->show_progress("Export Animation", anim_duration()); + int fps = App::I->animation->get_fps(); + MP4Encoder mp4; + int res = std::min(1024, m_width); + mp4.init(res * 4, res * 2, 30, 2 << 20); + for (int i = 0; i < anim_duration(); i++) + { + Image data; + App::I->render_task([&] + { + anim_goto_frame(i); + draw_merge(false); + Texture2D equirect = m_layers_merge.gen_equirect({ res, res }); + data = equirect.get_image(); + }); + for (int j = 0; j < 30/fps; j++) + mp4.encode(data); + pb->increment(); + } + mp4.write_mp4(path); + pp::panopainter::close_legacy_dialog_node(*pb); +} + +void Canvas::export_cube_faces(std::string file_name, std::function on_complete) +{ + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + export_cube_faces_thread(file_name); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); + }); + } +} + +void Canvas::export_cube_faces_thread(std::string file_name) +{ +#ifdef __OBJC__ + NSMutableArray* files = [NSMutableArray array]; +#endif + static std::array plane_names{ "front", "right", "back", "left", "top", "bottom" }; + + auto pb = App::I->show_progress("Export Cube Faces", 7); + + App::I->render_task([this] { + draw_merge(false); + }); + pb->increment(); + + for (int i = 0; i < 6; i++) + { + Image face = m_layers_merge.rtt(i).get_image(); + std::string path = fmt::format("{}/{}-{}.png", App::I->work_path, file_name, plane_names[i]); + face.save_png(path); + pb->increment(); + + App::I->publish_exported_image(path); +#ifdef __OBJC__ + [files addObject : [NSString stringWithUTF8String:path.c_str()] ]; +#endif + } + + pp::panopainter::close_legacy_dialog_node(*pb); + +#ifdef __OBJC__ + static char name[128]; + sprintf(name, "%s.zip", App::I->work_path.c_str()); + auto zip_path = [NSString stringWithUTF8String:name]; + //[SSZipArchive createZipFileAtPath:zip_path withFilesAtPaths:files]; + //for (NSString* f : files) + // [[NSFileManager defaultManager] removeItemAtPath:f error:nil]; +#endif +} + +void Canvas::project_save(std::function on_complete) +{ + if (App::I->check_license()) + { + const auto file_path = App::I->doc_path; + App::I->runtime().canvas_async_task([this, file_path, on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + bool ret = project_save_thread(file_path, true); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); }); + }); + } +} + +void Canvas::project_save(std::string file_path, std::function on_complete) +{ + LOG("saving %s", file_path.c_str()); + if (App::I->check_license()) + { + App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + bool ret = project_save_thread(file_path, true); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); }); + }); + } + else + { + LOG("no license, no save"); + } +} + +bool Canvas::project_save_thread(std::string file_path, bool show_progress) +{ + // already saved, nothing to do + if (!m_unsaved && file_path == App::I->doc_path) + { + LOG("already saved"); + return true; + } + +// static char name[128]; +// sprintf(name, "%s/latlong.ppi", data_path.c_str()); + FILE* fp = nullptr; + + const auto save_target = pp::app::plan_document_canvas_project_save_target(App::I->data_path, file_path); + if (!save_target) { + LOG("cannot plan project save target for %s: %s", file_path.c_str(), save_target.status().message); + return false; + } + const auto& save_paths = save_target.value(); + const std::string& file_name = save_paths.file_name; + const std::string& tmp_path = save_paths.temporary_path; + const std::string& lapse_path = save_paths.timelapse_path; + + LOG("file name %s", file_name.c_str()); + LOG("tmp path %s", tmp_path.c_str()); + + bool target_exists = false; + if ((fp = fopen(file_path.c_str(), "rb"))) { + fclose(fp); + fp = nullptr; + target_exists = true; + } + + const auto write_plan = pp::app::plan_document_canvas_project_save_write(save_paths, target_exists); + if (!write_plan) { + LOG("cannot plan project save write for %s: %s", file_path.c_str(), write_plan.status().message); + return false; + } + + bool use_tmp = write_plan.value().uses_temporary; + if (write_plan.value().uses_temporary) + { + LOG("use tmp file"); + fp = fopen(write_plan.value().write_path.c_str(), "wb"); + if (!fp) + { + LOG("cannot write tmp project to %s", tmp_path.c_str()); + use_tmp = false; + } + } + + LOG("save first time"); + + if (!fp) + { + // write directly to the new file + if (!(fp = fopen(file_path.c_str(), "wb"))) + { + LOG("cannot write project to %s", file_path.c_str()); + return false; + } + LOG("unsafe mode saving directly to %s", file_path.c_str()); + } + + PPIHeader ppi_header; + fwrite(&ppi_header, sizeof(PPIHeader), 1, fp); + + // load thumbnail + Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height); + + std::shared_ptr pb; + if (show_progress) + pb = App::I->show_progress("Saving Pano Project"); + + thumb.flip(); + fwrite(thumb.data(), thumb.size(), 1, fp); + + fwrite(&m_width, sizeof(int), 1, fp); + fwrite(&m_height, sizeof(int), 1, fp); + + int n_layers = (int)m_layers.size(); + fwrite(&n_layers, sizeof(int), 1, fp); + + int n_frames = std::accumulate(m_layers.begin(), m_layers.end(), 0, + [](int tot, auto& l) { return tot + l->frames_count(); }); + if (ppi_header.doc_version.minor >= 3) + fwrite(&n_frames, sizeof(int), 1, fp); + + int progress = 0; + int total = n_frames * 6; + + for (int i = 0; i < (int)m_layers.size(); i++) + { + int n_order = i; + fwrite(&n_order, sizeof(int), 1, fp); + + float layer_alpha = m_layers[i]->m_opacity; + fwrite(&layer_alpha, sizeof(float), 1, fp); + + int name_len = (int)m_layers[i]->m_name.size(); + fwrite(&name_len, sizeof(int), 1, fp); + fwrite(m_layers[i]->m_name.data(), name_len, 1, fp); + + if (ppi_header.doc_version.minor >= 2) + { + fwrite(&m_layers[i]->m_blend_mode, sizeof(int), 1, fp); + fwrite(&m_layers[i]->m_alpha_locked, sizeof(bool), 1, fp); + fwrite(&m_layers[i]->m_visible, sizeof(bool), 1, fp); + } + + int frames = 1; + if (ppi_header.doc_version.minor >= 3) + { + frames = (int)m_layers[i]->frames_count(); + fwrite(&frames, sizeof(int), 1, fp); + } + + for (int fi = 0; fi < frames; fi++) + { + if (ppi_header.doc_version.minor >= 3) + { + int duration = m_layers[i]->frame_duration(fi); + fwrite(&duration, sizeof(int), 1, fp); + } + bool gpu = m_layers[i]->frame(fi).gpu_load(); + m_layers[i]->optimize(fi); + auto snap = m_layers[i]->snapshot(fi); + for (int plane_index = 0; plane_index < 6; plane_index++) + { + int has_data = snap.m_dirty_face[plane_index] ? 1 : 0; + fwrite(&has_data, sizeof(int), 1, fp); + if (has_data) + { + glm::ivec4 b = snap.m_dirty_box[plane_index]; + glm::vec2 sz = zw(b) - xy(b); + int box[4] = { b.x, b.y, b.z, b.w }; + fwrite(&box, sizeof(box), 1, fp); + + std::vector compressed; + auto callback = [](void* context, void* data, int size) + { + std::vector* buffer = static_cast*>(context); + buffer->insert(buffer->end(), (uint8_t*)data, (uint8_t*)data + size); + }; + int ret = stbi_write_png_to_func(callback, &compressed, sz.x, sz.y, 4, snap.image[plane_index].get(), sz.x * 4); + + int data_size = (int)compressed.size(); + fwrite(&data_size, sizeof(int), 1, fp); + + fwrite(compressed.data(), 1, compressed.size(), fp); + } + progress++; + float p = (float)progress / total * 100.f; + if (show_progress) + pb->m_progress->SetWidthP(p); + LOG("progress: %f", p); + } + if (!gpu) + m_layers[i]->frame(fi).gpu_unload(); + } + } + + if (ppi_header.doc_version.minor >= 4) + { + BinaryStreamWriter sw; + sw.init(BinaryStream::ByteOrder::LittleEndian); + + Serializer::Descriptor info; + info.class_id = "ppi_info"; + info.name = L"info header"; + //info.props["has_encoder"] = std::make_shared(m_encoder != nullptr); + sw << info; + //if (m_encoder != nullptr) + // sw << *m_encoder; + + int bytes = sw.m_data.size(); + fwrite(&bytes, sizeof(int), 1, fp); + fwrite((char*)sw.m_data.data(), sw.m_data.size(), 1, fp); + } + + fclose(fp); + + bool target_remove_attempted = false; + bool target_remove_succeeded = false; + bool temporary_rename_attempted = false; + bool temporary_rename_succeeded = false; + if (use_tmp) + { + LOG("project saved tmp to %s", tmp_path.c_str()); + LOG("swapping to %s", file_path.c_str()); + target_remove_attempted = true; + target_remove_succeeded = std::remove(file_path.c_str()) == 0; + if (target_remove_succeeded) + { + temporary_rename_attempted = true; + temporary_rename_succeeded = std::rename(tmp_path.c_str(), file_path.c_str()) == 0; + } + } + + const auto commit_plan = pp::app::plan_document_canvas_project_save_commit( + pp::app::DocumentCanvasProjectSaveCommitInput { + .used_temporary = use_tmp, + .target_remove_attempted = target_remove_attempted, + .target_remove_succeeded = target_remove_succeeded, + .temporary_rename_attempted = temporary_rename_attempted, + .temporary_rename_succeeded = temporary_rename_succeeded, + }); + const bool success = commit_plan.saved; + if (commit_plan.saved && commit_plan.temporary_renamed) { + LOG("tmp file swapped succesfully"); + } else if (!commit_plan.saved && commit_plan.target_may_be_missing) { + LOG("tmp file NOT swapped, original removed"); + } else if (!commit_plan.saved && commit_plan.used_temporary) { + LOG("could not remove %s", file_path.c_str()); + } else if (commit_plan.saved) { + LOG("project saved to %s", file_path.c_str()); + } + + const auto post_commit_plan = pp::app::plan_document_canvas_project_save_post_commit( + pp::app::DocumentCanvasProjectSavePostCommitInput { + .save_succeeded = success, + .timelapse_encoder_available = Canvas::I->m_encoder != nullptr, + .progress_ui_visible = show_progress, + }); + + if (post_commit_plan.marks_document_clean) + { + m_unsaved = false; + } + if (post_commit_plan.marks_new_document_committed) + { + m_newdoc = false; + } + + if (post_commit_plan.saves_timelapse_sidecar) + { + BinaryStreamWriter sw; + sw.init(BinaryStream::ByteOrder::LittleEndian); + Serializer::Descriptor info; + info.class_id = "tracks-info"; + info.name = L"Timelapse Tracks"; + info.props["has-track-360"] = std::make_shared(true); + info.props["version"] = std::make_shared(1); + sw << info; + sw << *Canvas::I->m_encoder; + if (!sw.save(lapse_path)) + LOG("cannot save timelase to %s", lapse_path.c_str()); + } + + if (post_commit_plan.flushes_platform_storage) + { + App::I->flush_platform_storage(); + } + + if (post_commit_plan.dismisses_progress_ui) + { + pp::panopainter::close_legacy_dialog_node(*pb); + } + if (post_commit_plan.updates_title) + { + App::I->title_update(); + } + + return success; +} + +void Canvas::project_open(std::string file_path, std::function on_complete) +{ + App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + bool result = project_open_thread(file_path); + if (on_complete) + App::I->ui_task([on_complete = std::move(on_complete), result]() mutable { on_complete(result); }); + }); +} + +bool Canvas::project_open_thread(std::string file_path) +{ + FILE* fp = fopen(file_path.c_str(), "rb"); + if (!fp) + { + LOG("cannot write project to %s", file_path.c_str()); + return false; // should probably return a bool + } + + PPIHeader ppi_header; + fread(&ppi_header, sizeof(PPIHeader), 1, fp); + + if (!ppi_header.valid()) + { + LOG("INVALID PPI HEADER"); + return false; + } + + std::shared_ptr pb; + if (App::I->layout.m_loaded) + { + pb = std::make_shared(); + pb->set_manager(&App::I->layout); + pb->init(); + pb->create(); + pb->loaded(); + pb->m_progress->SetWidthP(0); + pb->m_title->set_text("Opening Pano Project"); + App::I->layout[App::I->main_id]->add_child(pb); + } + + // skip thumbnail + Image thumb; + thumb.width = ppi_header.thumb_header.width; + thumb.height = ppi_header.thumb_header.height; + thumb.comp = ppi_header.thumb_header.comp; + fseek(fp, thumb.size(), SEEK_CUR); + + fread(&m_width, sizeof(int), 1, fp); + fread(&m_height, sizeof(int), 1, fp); + + int n_layers = 0; + fread(&n_layers, sizeof(int), 1, fp); + int n_frames = 1; + if (ppi_header.doc_version.minor >= 3) + fread(&n_frames, sizeof(int), 1, fp); + + const int bytes = m_width * m_height * 4; + LayerFrame::Snapshot snap; + snap.create(m_width, m_height); // allocate single data, no box should be bigger + + int progress = 0; + int total = n_frames * 6; + + for (auto& l : m_layers) + l->destroy(); + m_layers.clear(); + //clear_all(); + resize(m_width, m_height); + + std::vector> tmp_layers(n_layers); + + for (int i = 0; i < n_layers; i++) + { + int n_order; + fread(&n_order, sizeof(int), 1, fp); + + //if (ppi_header.doc_version.minor > 1) + // n_order = i; + + tmp_layers[n_order] = std::make_unique(); + auto& layer = tmp_layers[n_order]; + + fread(&layer->m_opacity, sizeof(float), 1, fp); + + int name_len; + fread(&name_len, sizeof(int), 1, fp); + std::string name(name_len, '\0'); + fread((char*)name.data(), name_len, 1, fp); + + if (ppi_header.doc_version.minor >= 2) + { + fread(&layer->m_blend_mode, sizeof(int), 1, fp); + fread(&layer->m_alpha_locked, sizeof(bool), 1, fp); + fread(&layer->m_visible, sizeof(bool), 1, fp); + } + + int frames = 1; + if (ppi_header.doc_version.minor >= 3) + fread(&frames, sizeof(int), 1, fp); + + layer->create(m_width, m_height, name.c_str()); + + for (int fi = 0; fi < frames; fi++) + { + if (fi > 0) + layer->add_frame(); + if (ppi_header.doc_version.minor >= 3) + { + int duration = layer->frame_duration(fi); + fread(&duration, sizeof(int), 1, fp); + } + snap.clear(); + for (int plane_index = 0; plane_index < 6; plane_index++) + { + int has_data; + fread(&has_data, sizeof(int), 1, fp); + snap.m_dirty_face[plane_index] = has_data; + if (has_data) + { + int b[4]; + fread(&b, sizeof(b), 1, fp); + snap.m_dirty_box[plane_index] = glm::vec4(b[0], b[1], b[2], b[3]); + glm::vec2 sz = zw(snap.m_dirty_box[plane_index]) - xy(snap.m_dirty_box[plane_index]); + + int data_size; + fread(&data_size, sizeof(int), 1, fp); + std::vector compressed(data_size); + + fread(compressed.data(), 1, data_size, fp); + int imgw, imgh, imgc; + uint8_t* rgba = stbi_load_from_memory(compressed.data(), data_size, &imgw, &imgh, &imgc, 4); + if (rgba) + { + std::copy(rgba, rgba + (imgw * imgh * 4), snap.image[plane_index].get()); + delete rgba; + } + } + + progress++; + float p = (float)progress / total * 100.f; + LOG("progress: %f", p); + + if (App::I->layout.m_loaded) + { + pb->m_progress->SetWidthP(p); + } + } + layer->restore(snap, fi); + } + } + + std::swap(tmp_layers, m_layers); + + if (ppi_header.doc_version.minor >= 4) + { + int bytes = 0; + fread(&bytes, sizeof(int), 1, fp); + std::vector data(bytes); + fread(data.data(), bytes, 1, fp); + BinaryStreamReader sr; + sr.init(data.data(), data.size(), BinaryStream::ByteOrder::LittleEndian); + Serializer::Descriptor info; + sr >> info; + //if (info.value("has_encoder")) + //{ + // m_encoder = std::make_unique(); + // sr >> *m_encoder; + // m_encoder->init(); + //} + //else + //{ + // timelapse_reset_encoder(); + //} + } + //else + //{ + // timelapse_reset_encoder(); + //} + + fclose(fp); + LOG("project restore from %s", file_path.c_str()); + + auto start = file_path.rfind('/') + 1; + std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi")); + std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl"; + if (Asset::exist(lapse_path)) + { + BinaryStreamReader sr; + sr.load(lapse_path, BinaryStream::ByteOrder::LittleEndian); + Serializer::Descriptor info; + sr >> info; + if (info.value("has-track-360")) + { + m_encoder = std::make_unique(); + sr >> *m_encoder; + m_encoder->init(); + } + } + else + { + timelapse_reset_encoder(); + } + + m_current_layer_idx = 0; + m_current_stroke = nullptr; + m_dual_stroke = nullptr; + m_show_tmp = false; + m_smask_active = false; + m_smask_mode = 0; + m_dirty = false; + m_commit_delayed = false; + m_dirty_stroke = false; + memset(m_dirty_face, 0, sizeof(bool) * 6); + memset(m_pick_ready, 0, sizeof(bool) * 6); + m_unsaved = false; + m_newdoc = false; + if (App::I->layout.m_loaded) + { + pp::panopainter::close_legacy_dialog_node(*pb); + App::I->ui_task([] { + App::I->title_update(); + App::I->update_rec_frames(); + Canvas::I->anim_update(); + App::I->animation->load_layers(); + }); + } + return true; +} + +Image Canvas::thumbnail_generate(int w, int h) +{ + Image image; + image.create(w, h); + + App::I->render_task([this, w, h, &image] + { + // save viewport and clear color states + const auto vp = query_canvas_viewport(); + const auto cc = query_canvas_clear_color(); + auto blend = query_canvas_capability(blend_state()); + + // prepare common states + apply_canvas_viewport(0, 0, w, h); + + RTT fb; + fb.create(w, h); + fb.bindFramebuffer(); + Plane m_face_plane; + m_face_plane.create<1>(2, 2); + Texture2D blendtex; + blendtex.create(w, h); + const auto layer_feedback = canvas_destination_feedback_plan(w, h); + const bool copy_layer_destination = !layer_feedback.reads_destination_color; + + // recalculate because of different aspect ratio than the m_proj matrix + glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f); + + fb.clear({ 1, 1, 1, 0 }); + for (int i = 0; i < 6; i++) + { + apply_canvas_capability(blend_state(), false); + auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1)); + + if (copy_layer_destination) + { + set_active_texture_unit(2); + blendtex.bind(); + m_sampler_nearest.bind(2); + } + m_sampler_nearest.bind(0); // nearest + for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) + { + if (!m_layers[layer_index]->m_visible || + m_layers[layer_index]->m_opacity == 0.f || + !m_layers[layer_index]->face(i)) + continue; + if (copy_layer_destination) + { + set_active_texture_unit(2); + copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h); + } + pp::panopainter::setup_legacy_canvas_draw_merge_texture_blend_shader( + pp::panopainter::LegacyCanvasDrawMergeTextureBlendUniforms { + .mvp = plane_mvp, + .texture_slot = 0, + .destination_texture_slot = 2, + .use_destination_texture = copy_layer_destination, + .blend_mode = m_layers[layer_index]->m_blend_mode, + .alpha = m_layers[layer_index]->m_opacity, + }); + set_active_texture_unit(0); + m_layers[layer_index]->rtt(i).bindTexture(); + m_face_plane.draw_fill(); + m_layers[layer_index]->rtt(i).unbindTexture(); + } + + if (copy_layer_destination) + { + set_active_texture_unit(2); + blendtex.unbind(); + } + + set_active_texture_unit(0); + blendtex.bind(); + // copy the content of the fb before drawing the grid + copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h); + + // draw the grid + pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader( + pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms { + .mvp = plane_mvp, + }); + m_face_plane.draw_fill(); + + // now blend with the background + apply_canvas_capability(blend_state(), true); + pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader( + pp::panopainter::LegacyCanvasDrawMergeTextureUniforms { + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + .texture_slot = 0, + }); + m_sampler.bind(0); // linear + m_plane.draw_fill(); + + blendtex.unbind(); + } + + fb.unbindFramebuffer(); + + // read the rendered image + fb.readTextureData((uint8_t*)image.data()); + + fb.destroy(); + blendtex.destroy(); + + // restore viewport and clear color states + blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false); + apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height); + apply_canvas_clear_color(cc); + set_active_texture_unit(0); + }); + + return image; +} + +Image Canvas::thumbnail_read(std::string file_path) +{ +// static char name[128]; +// sprintf(name, "%s/latlong.ppi", data_path.c_str()); + FILE* fp = fopen(file_path.c_str(), "rb"); + if (!fp) + { + LOG("cannot read project %s", file_path.c_str()); + return {}; // return empty image + } + PPIHeader ppi_header; + fread(&ppi_header, sizeof(PPIHeader), 1, fp); + + if (!ppi_header.valid()) + return {}; + + Image thumb; + thumb.width = ppi_header.thumb_header.width; + thumb.height = ppi_header.thumb_header.height; + thumb.comp = ppi_header.thumb_header.comp; + thumb.create(); + fread((uint8_t*)thumb.data(), thumb.size(), 1, fp); + fclose(fp); + LOG("project thumbnail read from %s", file_path.c_str()); + return thumb; +} + diff --git a/src/legacy_node_stroke_preview_execution_services.h b/src/legacy_node_stroke_preview_execution_services.h index 5f720e22..4484434f 100644 --- a/src/legacy_node_stroke_preview_execution_services.h +++ b/src/legacy_node_stroke_preview_execution_services.h @@ -148,6 +148,40 @@ struct LegacyNodeStrokePreviewPassOrchestrationPlan { bool background_colorize = false; }; +struct LegacyNodeStrokePreviewRenderTargetSetup { + RTT& preview_rtt; + RTT& preview_rtt_mixer; + Texture2D& preview_stroke_texture; + Texture2D& preview_dual_texture; + Texture2D& preview_background_texture; + Texture2D& preview_image_texture; + glm::vec2 size {}; +}; + +[[nodiscard]] inline bool ensure_legacy_node_stroke_preview_render_targets( + const LegacyNodeStrokePreviewRenderTargetSetup& setup) +{ + if (setup.size.x <= 0.0f || setup.size.y <= 0.0f) { + return false; + } + + const auto width = static_cast(setup.size.x); + const auto height = static_cast(setup.size.y); + + if (!setup.preview_image_texture.ready() || setup.preview_image_texture.size() != setup.size) { + setup.preview_image_texture.create(width, height); + } + if (setup.preview_stroke_texture.size() != setup.size) { + setup.preview_rtt.create(width, height); + setup.preview_rtt_mixer.create(width, height); + setup.preview_stroke_texture.create(width, height); + setup.preview_dual_texture.create(width, height); + setup.preview_background_texture.create(width, height); + } + + return true; +} + struct LegacyNodeStrokePreviewMainPassTextureDispatch { std::function activate_texture_unit; std::function bind_brush_tip; @@ -245,6 +279,34 @@ template return true; } +struct LegacyNodeStrokePreviewImmediatePassSequenceRequest { + std::function execute_dual_pass; + std::function capture_background; + std::function execute_main_live_pass; + std::function execute_final_composite; +}; + +[[nodiscard]] inline bool execute_legacy_node_stroke_preview_immediate_pass_sequence( + const LegacyNodeStrokePreviewImmediatePassSequenceRequest& request) +{ + if (!request.capture_background || + !request.execute_main_live_pass || + !request.execute_final_composite) { + return false; + } + + if (request.execute_dual_pass) { + request.execute_dual_pass(); + } + + request.capture_background(); + if (!request.execute_main_live_pass()) { + return false; + } + + return request.execute_final_composite(); +} + [[nodiscard]] inline bool execute_legacy_node_stroke_preview_final_composite( glm::vec2 size, glm::vec2 pattern_scale, diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 2d32690d..f7ef6abc 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -588,6 +588,19 @@ void NodeStrokePreview::draw_stroke_immediate() if (m_size.x == 0 || m_size.y == 0) return; + if (!pp::panopainter::ensure_legacy_node_stroke_preview_render_targets( + pp::panopainter::LegacyNodeStrokePreviewRenderTargetSetup { + .preview_rtt = m_rtt, + .preview_rtt_mixer = m_rtt_mixer, + .preview_stroke_texture = m_tex, + .preview_dual_texture = m_tex_dual, + .preview_background_texture = m_tex_background, + .preview_image_texture = m_tex_preview, + .size = m_preview_size, + })) { + return; + } + const auto vp = query_stroke_preview_viewport(); const auto cc = query_stroke_preview_clear_color(); @@ -665,8 +678,6 @@ void NodeStrokePreview::draw_stroke_immediate() m_dual_stroke.add_point(point.position, point.pressure); } - const glm::vec2 patt_scale = stroke_setup.pattern_scale; - apply_stroke_preview_capability(pp::renderer::gl::blend_state(), false); const auto pass_orchestration = pp::panopainter::plan_legacy_node_stroke_preview_pass_orchestration( pp::panopainter::LegacyNodeStrokePreviewPassOrchestrationRequest { @@ -696,64 +707,64 @@ void NodeStrokePreview::draw_stroke_immediate() const auto& material = pass_orchestration.material; pp::panopainter::setup_legacy_stroke_shader(pass_orchestration.stroke_shader); - const bool sequence_ok = [&] { - if (pass_orchestration.material.dual_pass.enabled) { - pp::panopainter::setup_legacy_stroke_dual_shader( - pass_orchestration.material.dual_pass.uses_pattern); - bind_stroke_preview_dual_pass_textures(*dual_brush); - execute_stroke_draw_immediate_dual_pass( - m_dual_stroke, - *b, - pass_orchestration, - std::move(dual_brush), - copy_stroke_destination, - zoom, - size); - } + const bool sequence_ok = pp::panopainter::execute_legacy_node_stroke_preview_immediate_pass_sequence( + pp::panopainter::LegacyNodeStrokePreviewImmediatePassSequenceRequest { + .execute_dual_pass = [&] { + if (!pass_orchestration.material.dual_pass.enabled) { + return; + } - execute_stroke_preview_background_capture_pass( - size, - pass_orchestration.background_colorize, - m_tex_background, - [&] { - m_plane.draw_fill(); - }); - - [[maybe_unused]] const bool main_live_ok = - pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass( - make_stroke_draw_immediate_main_live_pass_request( - m_stroke, + pp::panopainter::setup_legacy_stroke_dual_shader( + pass_orchestration.material.dual_pass.uses_pattern); + bind_stroke_preview_dual_pass_textures(*dual_brush); + execute_stroke_draw_immediate_dual_pass( + m_dual_stroke, *b, pass_orchestration, + std::move(dual_brush), copy_stroke_destination, zoom, - size)); - if (!main_live_ok) { - return false; - } - - const bool final_composite_ok = pp::panopainter::execute_legacy_node_stroke_preview_final_composite( - size, - glm::vec2(b->m_pattern_scale), - *b, - material.composite_pass, - m_tex_background, - m_tex, - m_tex_dual, - m_tex_preview, - m_sampler_linear, - m_sampler_linear_repeat, - [&] { - b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); + size); }, - [&] { - m_plane.draw_fill(); - }); - if (!final_composite_ok) { - return false; - } - return true; - }(); + .capture_background = [&] { + execute_stroke_preview_background_capture_pass( + size, + pass_orchestration.background_colorize, + m_tex_background, + [&] { + m_plane.draw_fill(); + }); + }, + .execute_main_live_pass = [&]() -> bool { + return pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass( + make_stroke_draw_immediate_main_live_pass_request( + m_stroke, + *b, + pass_orchestration, + copy_stroke_destination, + zoom, + size)); + }, + .execute_final_composite = [&]() -> bool { + return pp::panopainter::execute_legacy_node_stroke_preview_final_composite( + size, + glm::vec2(b->m_pattern_scale), + *b, + material.composite_pass, + m_tex_background, + m_tex, + m_tex_dual, + m_tex_preview, + m_sampler_linear, + m_sampler_linear_repeat, + [&] { + b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); + }, + [&] { + m_plane.draw_fill(); + }); + }, + }); assert(sequence_ok); m_rtt.unbindFramebuffer(); @@ -888,17 +899,6 @@ Image NodeStrokePreview::render_to_image() std::lock_guard _lock(s_render_mutex); App::I->render_task([this] { - auto new_size = m_preview_size; - if (!m_tex_preview.ready() || m_tex_preview.size() != new_size) - m_tex_preview.create((int)new_size.x, (int)new_size.y); - if (m_tex.size() != new_size) - { - m_rtt.create((int)new_size.x, (int)new_size.y); - m_rtt_mixer.create((int)new_size.x, (int)new_size.y); - m_tex.create((int)new_size.x, (int)new_size.y); - m_tex_dual.create((int)new_size.x, (int)new_size.y); - m_tex_background.create((int)new_size.x, (int)new_size.y); - } draw_stroke_immediate(); }); return m_tex_preview.get_image(); @@ -940,18 +940,6 @@ void NodeStrokePreview::draw_stroke() gl_state gl; gl.save(); - auto new_size = node->m_preview_size; - if (!node->m_tex_preview.ready() || node->m_tex_preview.size() != new_size) - node->m_tex_preview.create((int)new_size.x, (int)new_size.y); - if (m_tex.size() != new_size) - { - m_rtt.create((int)new_size.x, (int)new_size.y); - m_rtt_mixer.create((int)new_size.x, (int)new_size.y); - m_tex.create((int)new_size.x, (int)new_size.y); - m_tex_dual.create((int)new_size.x, (int)new_size.y); - m_tex_background.create((int)new_size.x, (int)new_size.y); - } - node->m_brush->load(); node->draw_stroke_immediate(); if (to_unload)