Add app document route core

This commit is contained in:
2026-06-02 22:10:50 +02:00
parent e15894e4ea
commit 0e03e5940a
10 changed files with 204 additions and 12 deletions

View File

@@ -210,6 +210,18 @@ target_link_libraries(pp_ui_core
pp_xml_tinyxml2 pp_xml_tinyxml2
pp_project_warnings) pp_project_warnings)
add_library(pp_app_core STATIC
src/app_core/document_route.cpp)
target_include_directories(pp_app_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_app_core
PUBLIC
pp_foundation
pp_project_options
PRIVATE
pp_project_warnings)
if(PP_BUILD_TOOLS) if(PP_BUILD_TOOLS)
add_subdirectory(tools/pano_cli) add_subdirectory(tools/pano_cli)
endif() endif()
@@ -461,6 +473,7 @@ if(PP_BUILD_APP)
"${CMAKE_CURRENT_SOURCE_DIR}/src") "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(panopainter_app target_link_libraries(panopainter_app
PUBLIC PUBLIC
pp_app_core
pp_legacy_app pp_legacy_app
pp_panopainter_ui pp_panopainter_ui
pp_project_options pp_project_options

View File

@@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets.
| Platform/Target | Current Entrypoint | Notes | | Platform/Target | Current Entrypoint | Notes |
| --- | --- | --- | | --- | --- | --- |
| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | | Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing owned by `pp_app_core`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` |
| Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging | | Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging |
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today | | macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today |
| iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today | | iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today |
@@ -391,6 +391,10 @@ Known local toolchain state:
- `pano_cli apply-stroke-script` exposes file-driven stroke-script application - `pano_cli apply-stroke-script` exposes file-driven stroke-script application
to a pure document face payload and writes a PPI artifact for inspect/load to a pure document face payload and writes a PPI artifact for inspect/load
round-trip automation. round-trip automation.
- `pp_app_core_document_route_tests` covers the app document-open route
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
and malformed paths before the live `App::open_document` performs UI or
legacy canvas work.
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
through the vcpkg preset; default and Android validation still use the through the vcpkg preset; default and Android validation still use the
retained vendored fallback tracked by DEBT-0012. retained vendored fallback tracked by DEBT-0012.
@@ -425,6 +429,9 @@ Known warnings after the current CMake app build:
header and static-analysis warnings while it still depends on header and static-analysis warnings while it still depends on
`pp_legacy_app`; these should be reduced as the UI core/app UI boundary is `pp_legacy_app`; these should be reduced as the UI core/app UI boundary is
tightened instead of suppressed globally. tightened instead of suppressed globally.
- `pp_app_core` is the first pure app-engine target consumed by
`panopainter_app`; it should grow only with UI-free command routing,
validation, and app service contracts that can be tested without a window.
- `panopainter_app` currently surfaces existing app orchestration, GLM, - `panopainter_app` currently surfaces existing app orchestration, GLM,
base64, VR, and serializer warnings now that app sources live in the base64, VR, and serializer warnings now that app sources live in the
composition target; warning cleanup should follow component ownership rather composition target; warning cleanup should follow component ownership rather

View File

@@ -12,6 +12,7 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests | | Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture | | PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture |
| Open-document routing | `App::open_document` | `pp_app_core`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, app open smoke |
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | | Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input | | Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input |
| Save-as, overwrite prompts | App/dialogs | `pp_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke | | Save-as, overwrite prompts | App/dialogs | `pp_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke |

View File

@@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat.
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets |
| DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation |
| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split | Avoid behavior changes while introducing component boundaries | App launch and component tests | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document` now consumes a pure `pp_app_core` route contract, but document loading still reaches legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries |
| DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path |
| DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated |
| DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions |

View File

@@ -165,8 +165,12 @@ owns retained base `Node`, layout, text, image, input, popup, slider, scroll,
and settings UI controls as an object-library boundary folded into the legacy and settings UI controls as an object-library boundary folded into the legacy
app adapter until those paths are replaced by `pp_ui_core` and app-specific UI app adapter until those paths are replaced by `pp_ui_core` and app-specific UI
targets. targets.
`panopainter_app` is now a real static target that owns app orchestration `pp_app_core` now owns tested app-level document-open routing for project
sources, app version metadata, and version-header generation. files, ABR imports, and PPBR imports without UI, filesystem, platform, or
renderer dependencies; `App::open_document` consumes this route contract while
legacy canvas/project loading remains in place. `panopainter_app` is now a real
static target that owns app orchestration sources, app version metadata, and
version-header generation.
`pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas,
viewport, color-picker, stroke-preview, and tool UI workflow nodes outside viewport, color-picker, stroke-preview, and tool UI workflow nodes outside
`pp_legacy_app`; base `Node` controls and layout plumbing remain in the legacy `pp_legacy_app`; base `Node` controls and layout plumbing remain in the legacy

View File

@@ -5,6 +5,7 @@
#include "node_dialog_open.h" #include "node_dialog_open.h"
#include "node_progress_bar.h" #include "node_progress_bar.h"
#include "mp4enc.h" #include "mp4enc.h"
#include "app_core/document_route.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
#ifdef __APPLE__ #ifdef __APPLE__
@@ -187,15 +188,11 @@ void App::create()
void App::open_document(std::string path) void App::open_document(std::string path)
{ {
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)"); const auto route = pp::app::classify_document_open_path(path);
std::smatch m; if (!route)
if (!std::regex_search(path, m, r))
return; return;
std::string base = m[1].str();
std::string name = m[2].str();
std::string ext = m[3].str();
if (str_iequals(ext, "abr")) if (route.value().kind == pp::app::DocumentOpenKind::import_abr)
{ {
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true); auto mb = message_box("Import ABR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) { mb->on_submit = [this, path] (Node* target) {
@@ -203,7 +200,7 @@ void App::open_document(std::string path)
target->destroy(); target->destroy();
}; };
} }
else if (str_iequals(ext, "ppbr")) else if (route.value().kind == pp::app::DocumentOpenKind::import_ppbr)
{ {
auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true); auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) { mb->on_submit = [this, path] (Node* target) {
@@ -213,6 +210,8 @@ void App::open_document(std::string path)
} }
else else
{ {
const std::string base = route.value().directory;
const std::string name = route.value().name;
auto open_action = [this, path, base, name] { auto open_action = [this, path, base, name] {
doc_name = name; doc_name = name;
doc_dir = base; doc_dir = base;

View File

@@ -0,0 +1,67 @@
#include "app_core/document_route.h"
#include <cctype>
#include <utility>
namespace pp::app {
namespace {
[[nodiscard]] bool is_extension_char(char value) noexcept
{
const auto ch = static_cast<unsigned char>(value);
return std::isalnum(ch) != 0 || value == '_';
}
[[nodiscard]] std::string lowercase_ascii(std::string_view value)
{
std::string lowered;
lowered.reserve(value.size());
for (const char ch : value) {
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
}
return lowered;
}
}
pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(std::string_view path)
{
const auto separator = path.find_last_of("/\\");
if (separator == std::string_view::npos || separator + 1U >= path.size()) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document path must include a directory and file name"));
}
const auto dot = path.find_last_of('.');
if (dot == std::string_view::npos || dot <= separator + 1U || dot + 1U >= path.size()) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document path must include a file extension"));
}
const std::string_view extension = path.substr(dot + 1U);
for (const char ch : extension) {
if (!is_extension_char(ch)) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document extension contains unsupported characters"));
}
}
auto lowered_extension = lowercase_ascii(extension);
auto kind = DocumentOpenKind::open_project;
if (lowered_extension == "abr") {
kind = DocumentOpenKind::import_abr;
} else if (lowered_extension == "ppbr") {
kind = DocumentOpenKind::import_ppbr;
}
return pp::foundation::Result<DocumentOpenRoute>::success(
DocumentOpenRoute {
.kind = kind,
.path = std::string(path),
.directory = std::string(path.substr(0U, separator)),
.name = std::string(path.substr(separator + 1U, dot - separator - 1U)),
.extension = std::move(lowered_extension),
});
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
namespace pp::app {
enum class DocumentOpenKind {
import_abr,
import_ppbr,
open_project,
};
struct DocumentOpenRoute {
DocumentOpenKind kind = DocumentOpenKind::open_project;
std::string path;
std::string directory;
std::string name;
std::string extension;
};
[[nodiscard]] pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(
std::string_view path);
}

View File

@@ -258,6 +258,16 @@ add_test(NAME pp_ui_core_layout_xml_tests COMMAND pp_ui_core_layout_xml_tests)
set_tests_properties(pp_ui_core_layout_xml_tests PROPERTIES set_tests_properties(pp_ui_core_layout_xml_tests PROPERTIES
LABELS "ui;desktop-fast;fuzz") LABELS "ui;desktop-fast;fuzz")
add_executable(pp_app_core_document_route_tests
app_core/document_route_tests.cpp)
target_link_libraries(pp_app_core_document_route_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_document_route_tests COMMAND pp_app_core_document_route_tests)
set_tests_properties(pp_app_core_document_route_tests PROPERTIES
LABELS "app;desktop-fast;fuzz")
if(TARGET pano_cli) if(TARGET pano_cli)
add_test(NAME pano_cli_create_document_smoke add_test(NAME pano_cli_create_document_smoke
COMMAND pano_cli create-document --width 64 --height 32 --layers 2) COMMAND pano_cli create-document --width 64 --height 32 --layers 2)

View File

@@ -0,0 +1,64 @@
#include "app_core/document_route.h"
#include "test_harness.h"
namespace {
void classifies_regular_project_path(pp::tests::Harness& harness)
{
const auto route = pp::app::classify_document_open_path("D:\\Paint\\Scenes\\demo.ppi");
PP_EXPECT(harness, route);
PP_EXPECT(harness, route.value().kind == pp::app::DocumentOpenKind::open_project);
PP_EXPECT(harness, route.value().path == "D:\\Paint\\Scenes\\demo.ppi");
PP_EXPECT(harness, route.value().directory == "D:\\Paint\\Scenes");
PP_EXPECT(harness, route.value().name == "demo");
PP_EXPECT(harness, route.value().extension == "ppi");
}
void classifies_brush_imports_case_insensitively(pp::tests::Harness& harness)
{
const auto abr_route = pp::app::classify_document_open_path("/brushes/Clouds.ABR");
const auto ppbr_route = pp::app::classify_document_open_path("/brushes/palette.PpBr");
PP_EXPECT(harness, abr_route);
PP_EXPECT(harness, ppbr_route);
PP_EXPECT(harness, abr_route.value().kind == pp::app::DocumentOpenKind::import_abr);
PP_EXPECT(harness, abr_route.value().extension == "abr");
PP_EXPECT(harness, ppbr_route.value().kind == pp::app::DocumentOpenKind::import_ppbr);
PP_EXPECT(harness, ppbr_route.value().extension == "ppbr");
}
void preserves_names_with_inner_dots(pp::tests::Harness& harness)
{
const auto route = pp::app::classify_document_open_path("C:/projects/shot.001.paint.ppi");
PP_EXPECT(harness, route);
PP_EXPECT(harness, route.value().directory == "C:/projects");
PP_EXPECT(harness, route.value().name == "shot.001.paint");
PP_EXPECT(harness, route.value().extension == "ppi");
}
void rejects_paths_that_legacy_regex_did_not_accept(pp::tests::Harness& harness)
{
const auto no_directory = pp::app::classify_document_open_path("demo.ppi");
const auto no_extension = pp::app::classify_document_open_path("D:/Paint/demo");
const auto empty_file = pp::app::classify_document_open_path("D:/Paint/.ppi");
const auto unsupported_extension_chars = pp::app::classify_document_open_path("D:/Paint/demo.ppi.tmp!");
PP_EXPECT(harness, !no_directory);
PP_EXPECT(harness, !no_extension);
PP_EXPECT(harness, !empty_file);
PP_EXPECT(harness, !unsupported_extension_chars);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("classifies regular project path", classifies_regular_project_path);
harness.run("classifies brush imports case insensitively", classifies_brush_imports_case_insensitively);
harness.run("preserves names with inner dots", preserves_names_with_inner_dots);
harness.run("rejects paths that legacy regex did not accept", rejects_paths_that_legacy_regex_did_not_accept);
return harness.finish();
}