Add app document route core
This commit is contained in:
@@ -210,6 +210,18 @@ target_link_libraries(pp_ui_core
|
||||
pp_xml_tinyxml2
|
||||
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)
|
||||
add_subdirectory(tools/pano_cli)
|
||||
endif()
|
||||
@@ -461,6 +473,7 @@ if(PP_BUILD_APP)
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(panopainter_app
|
||||
PUBLIC
|
||||
pp_app_core
|
||||
pp_legacy_app
|
||||
pp_panopainter_ui
|
||||
pp_project_options
|
||||
|
||||
@@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets.
|
||||
|
||||
| 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 |
|
||||
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` 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
|
||||
to a pure document face payload and writes a PPI artifact for inspect/load
|
||||
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`
|
||||
through the vcpkg preset; default and Android validation still use the
|
||||
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
|
||||
`pp_legacy_app`; these should be reduced as the UI core/app UI boundary is
|
||||
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,
|
||||
base64, VR, and serializer warnings now that app sources live in the
|
||||
composition target; warning cleanup should follow component ownership rather
|
||||
|
||||
@@ -12,6 +12,7 @@ and validation command.
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -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-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-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 |
|
||||
|
||||
@@ -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
|
||||
app adapter until those paths are replaced by `pp_ui_core` and app-specific UI
|
||||
targets.
|
||||
`panopainter_app` is now a real static target that owns app orchestration
|
||||
sources, app version metadata, and version-header generation.
|
||||
`pp_app_core` now owns tested app-level document-open routing for project
|
||||
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,
|
||||
viewport, color-picker, stroke-preview, and tool UI workflow nodes outside
|
||||
`pp_legacy_app`; base `Node` controls and layout plumbing remain in the legacy
|
||||
|
||||
15
src/app.cpp
15
src/app.cpp
@@ -5,6 +5,7 @@
|
||||
#include "node_dialog_open.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "mp4enc.h"
|
||||
#include "app_core/document_route.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -187,15 +188,11 @@ void App::create()
|
||||
|
||||
void App::open_document(std::string path)
|
||||
{
|
||||
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
|
||||
std::smatch m;
|
||||
if (!std::regex_search(path, m, r))
|
||||
const auto route = pp::app::classify_document_open_path(path);
|
||||
if (!route)
|
||||
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);
|
||||
mb->on_submit = [this, path] (Node* target) {
|
||||
@@ -203,7 +200,7 @@ void App::open_document(std::string path)
|
||||
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);
|
||||
mb->on_submit = [this, path] (Node* target) {
|
||||
@@ -213,6 +210,8 @@ void App::open_document(std::string path)
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string base = route.value().directory;
|
||||
const std::string name = route.value().name;
|
||||
auto open_action = [this, path, base, name] {
|
||||
doc_name = name;
|
||||
doc_dir = base;
|
||||
|
||||
67
src/app_core/document_route.cpp
Normal file
67
src/app_core/document_route.cpp
Normal 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),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
27
src/app_core/document_route.h
Normal file
27
src/app_core/document_route.h
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
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)
|
||||
add_test(NAME pano_cli_create_document_smoke
|
||||
COMMAND pano_cli create-document --width 64 --height 32 --layers 2)
|
||||
|
||||
64
tests/app_core/document_route_tests.cpp
Normal file
64
tests/app_core/document_route_tests.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user