Route layer panel view through app core

This commit is contained in:
2026-06-05 00:50:37 +02:00
parent bd6cdc20c5
commit 75fd7faeb0
8 changed files with 368 additions and 7 deletions

View File

@@ -215,6 +215,11 @@ Known local toolchain state:
operations. It keeps those live paths on the `pp_app_core` contracts while operations. It keeps those live paths on the `pp_app_core` contracts while
legacy `Canvas`, `NodeLayer`, `NodePanelLayer`, and `ActionManager` legacy `Canvas`, `NodeLayer`, `NodePanelLayer`, and `ActionManager`
execution remain tracked by `DEBT-0021` and `DEBT-0032`. execution remain tracked by `DEBT-0021` and `DEBT-0032`.
- `NodePanelLayer::update_attributes()` now consumes the tested
`pp_app_core` layer panel view model for current opacity, alpha-lock, blend
mode, and per-layer visibility projection. `pano_cli plan-layer-panel-view`
exposes the same view model for automation; retained canvas mutation, UI node
ownership, and undo behavior remain tracked by `DEBT-0021`.
- `src/legacy_app_shell_services.*` is the current app-shell bridge for File - `src/legacy_app_shell_services.*` is the current app-shell bridge for File
menu routing, export-menu routing, main-toolbar commands, About menu menu routing, export-menu routing, main-toolbar commands, About menu
commands, and direct Tools menu commands. It keeps those live paths on the commands, and direct Tools menu commands. It keeps those live paths on the

View File

@@ -45,6 +45,12 @@ agent or engineer to remove them without reconstructing context from chat.
projection now also uses a tested `pp_app_core` view model exposed by projection now also uses a tested `pp_app_core` view model exposed by
`pano_cli plan-animation-panel-view`, including stale-selection behavior. `pano_cli plan-animation-panel-view`, including stale-selection behavior.
Legacy canvas/layer/UI execution remains open under DEBT-0022. Legacy canvas/layer/UI execution remains open under DEBT-0022.
- 2026-06-05: DEBT-0021 was narrowed again. Layer panel selected-control and
visibility view projection now goes through tested `pp_app_core` planning,
`NodePanelLayer::update_attributes()` consumes that view model in the live
app, and `pano_cli plan-layer-panel-view` exposes the same path for
automation. Legacy layer mutation, UI node ownership, and undo wiring remain
open under DEBT-0021.
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
thumbnail, and object-draw history paths now query saved blend state through thumbnail, and object-draw history paths now query saved blend state through
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
@@ -79,7 +85,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0017 | Open | Modernization | Startup storage path preparation, `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render platform hint hooks, render debug callback hooks, render-capture frame hooks, recording cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, working-directory picker/display-path policy, canvas input tip/pressure policy, prepared-file save/download handoff, work-directory document export collection policy, app network TLS verification policy, PPBR export data-directory policy, SonarPen availability/startup, and VR mode start/stop now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; Windows render-platform hint and debug-output state token/enable sequencing now delegates to tested `pp_renderer_gl` helpers, leaving Windows with context, callback, console, and Win32 ownership; the retained macOS fallback render-platform hint enable sequence also delegates to the same tested `pp_renderer_gl` helper; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches, including retained iOS canvas tip behavior, retained macOS directory picker/display-path behavior, retained iOS SonarPen bridge, retained non-Windows VR unsupported/no-op behavior, and retained macOS PPBR export directory override; `pp_platform_api` also owns the default network TLS policy helper consumed by retained curl sites that cannot yet depend on injected services | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell | | DEBT-0017 | Open | Modernization | Startup storage path preparation, `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render platform hint hooks, render debug callback hooks, render-capture frame hooks, recording cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, working-directory picker/display-path policy, canvas input tip/pressure policy, prepared-file save/download handoff, work-directory document export collection policy, app network TLS verification policy, PPBR export data-directory policy, SonarPen availability/startup, and VR mode start/stop now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; Windows render-platform hint and debug-output state token/enable sequencing now delegates to tested `pp_renderer_gl` helpers, leaving Windows with context, callback, console, and Win32 ownership; the retained macOS fallback render-platform hint enable sequence also delegates to the same tested `pp_renderer_gl` helper; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches, including retained iOS canvas tip behavior, retained macOS directory picker/display-path behavior, retained iOS SonarPen bridge, retained non-Windows VR unsupported/no-op behavior, and retained macOS PPBR export directory override; `pp_platform_api` also owns the default network TLS policy helper consumed by retained curl sites that cannot yet depend on injected services | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell |
| DEBT-0019 | Open | Modernization | Unreferenced-parameter warnings are muted globally through `pp_project_warnings` with MSVC `/wd4100` and Clang/GCC `-Wno-unused-parameter` | Legacy callbacks, virtual hooks, serializer methods, and platform/API compatibility functions carry many intentionally unused parameters during the component split; muting this keeps stricter warning builds focused on higher-signal migration issues | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset linux-clang --target pp_foundation` | Remove `/wd4100` and `-Wno-unused-parameter`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app plus headless Clang/GCC tests pass without unreferenced-parameter warnings | | DEBT-0019 | Open | Modernization | Unreferenced-parameter warnings are muted globally through `pp_project_warnings` with MSVC `/wd4100` and Clang/GCC `-Wno-unused-parameter` | Legacy callbacks, virtual hooks, serializer methods, and platform/API compatibility functions carry many intentionally unused parameters during the component split; muting this keeps stricter warning builds focused on higher-signal migration issues | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset linux-clang --target pp_foundation` | Remove `/wd4100` and `-Wno-unused-parameter`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app plus headless Clang/GCC tests pass without unreferenced-parameter warnings |
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history through the history bridge | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing | | DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history through the history bridge | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing |
| DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch and layer panel operation planning/execution dispatch now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely | | DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch, layer panel operation planning/execution dispatch, and layer panel selected-control/visibility view projection now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `NodePanelLayer::update_attributes()`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `pano_cli plan-layer-panel-view`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge and panel adapter still mutate legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | Preserve existing UI/canvas behavior while document layer commands, panel projection, and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `pano_cli plan-layer-panel-view --layer-count 3 --current-index 1 --hidden-index 2 --locked-index 1 --current-opacity 0.25 --current-blend-mode 4`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution and panel state projection are owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel view-model projection, timeline scrub planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-panel-view`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --current-frame 4`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely | | DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel view-model projection, timeline scrub planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-panel-view`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --current-frame 4`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, refreshes legacy quick/stroke/color widgets, and uses temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset command boundary and asset-managed texture/preset selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-preset-list --kind remove --item-count 1 --current-index 0`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, or brush-panel friend access | | DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, refreshes legacy quick/stroke/color widgets, and uses temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset command boundary and asset-managed texture/preset selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-preset-list --kind remove --item-count 1 --current-index 0`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, or brush-panel friend access |
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter |

View File

@@ -515,6 +515,10 @@ duplicate, select, reorder, remove, opacity, visibility, alpha-lock, blend-mode,
and highlight actions used by the live layer panel. Direct layer-panel and highlight actions used by the live layer panel. Direct layer-panel
operations now dispatch through `DocumentLayerOperationServices` before the operations now dispatch through `DocumentLayerOperationServices` before the
shared app-shell layer bridge continues legacy `Canvas` and UI layer execution. shared app-shell layer bridge continues legacy `Canvas` and UI layer execution.
`pano_cli plan-layer-panel-view` exposes the app-core layer panel view model for
current opacity, alpha-lock, blend mode, and per-layer visibility state, and
live `NodePanelLayer::update_attributes()` now consumes that tested projection
before writing the retained legacy UI controls.
`pano_cli plan-layer-menu` exposes app-core planning for Layer menu clear, `pano_cli plan-layer-menu` exposes app-core planning for Layer menu clear,
rename, and merge-down labels/actions, and direct Layer menu commands now rename, and merge-down labels/actions, and direct Layer menu commands now
dispatch through `DocumentLayerMenuServices` before the legacy canvas/layer UI dispatch through `DocumentLayerMenuServices` before the legacy canvas/layer UI

View File

@@ -8,6 +8,7 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility> #include <utility>
#include <vector>
namespace pp::app { namespace pp::app {
@@ -84,6 +85,33 @@ struct DocumentLayerMergePlan {
bool create_history = true; bool create_history = true;
}; };
struct DocumentLayerPanelInput {
int layer_index = 0;
std::string name;
float opacity = 1.0F;
bool visible = true;
bool alpha_locked = false;
int blend_mode = 0;
};
struct DocumentLayerPanelLayerView {
int layer_index = 0;
std::string name;
float opacity = 1.0F;
bool visible = true;
bool alpha_locked = false;
int blend_mode = 0;
bool current = false;
};
struct DocumentLayerPanelView {
int current_index = 0;
float current_opacity = 1.0F;
bool current_alpha_locked = false;
int current_blend_mode = 0;
std::vector<DocumentLayerPanelLayerView> layers;
};
class DocumentLayerMenuServices { class DocumentLayerMenuServices {
public: public:
virtual ~DocumentLayerMenuServices() = default; virtual ~DocumentLayerMenuServices() = default;
@@ -158,6 +186,60 @@ public:
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
[[nodiscard]] inline pp::foundation::Result<DocumentLayerPanelView> plan_document_layer_panel_view(
const std::vector<DocumentLayerPanelInput>& layers,
int current_index)
{
if (layers.empty()) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(
pp::foundation::Status::invalid_argument("layer panel requires at least one layer"));
}
const auto current_status = validate_layer_index(static_cast<int>(layers.size()), current_index);
if (!current_status.ok()) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(current_status);
}
DocumentLayerPanelView view;
view.current_index = current_index;
view.layers.reserve(layers.size());
for (const auto& input : layers) {
const auto index_status = validate_layer_index(static_cast<int>(layers.size()), input.layer_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(index_status);
}
if (!std::isfinite(input.opacity) || input.opacity < 0.0F || input.opacity > 1.0F) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
}
if (input.blend_mode < 0 || input.blend_mode >= document_layer_legacy_blend_mode_count) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
}
const bool current = input.layer_index == current_index;
DocumentLayerPanelLayerView layer;
layer.layer_index = input.layer_index;
layer.name = input.name;
layer.opacity = input.opacity;
layer.visible = input.visible;
layer.alpha_locked = input.alpha_locked;
layer.blend_mode = input.blend_mode;
layer.current = current;
if (current) {
view.current_opacity = input.opacity;
view.current_alpha_locked = input.alpha_locked;
view.current_blend_mode = input.blend_mode;
}
view.layers.push_back(std::move(layer));
}
return pp::foundation::Result<DocumentLayerPanelView>::success(std::move(view));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename( [[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
std::string_view old_name, std::string_view old_name,
std::string_view requested_name) std::string_view requested_name)

View File

@@ -1,4 +1,5 @@
#include "pch.h" #include "pch.h"
#include "app_core/document_layer.h"
#include "log.h" #include "log.h"
#include "node_panel_layer.h" #include "node_panel_layer.h"
#include "canvas.h" #include "canvas.h"
@@ -367,13 +368,38 @@ void NodePanelLayer::clear()
void NodePanelLayer::update_attributes() void NodePanelLayer::update_attributes()
{ {
auto& l = Canvas::I->m_layers[Canvas::I->m_current_layer_idx]; if (!Canvas::I)
m_opacity->set_value(l->m_opacity); return;
m_alpha_lock->set_value(l->m_alpha_locked);
m_blend_mode->set_index(l->m_blend_mode); std::vector<pp::app::DocumentLayerPanelInput> layer_inputs;
for (int i = 0; i < Canvas::I->m_layers.size(); i++) layer_inputs.reserve(Canvas::I->m_layers.size());
for (int i = 0; i < static_cast<int>(Canvas::I->m_layers.size()); i++)
{ {
m_layers[i]->m_visibility->set_value(Canvas::I->m_layers[i]->m_visible); const auto& layer = Canvas::I->m_layers[i];
layer_inputs.push_back(pp::app::DocumentLayerPanelInput {
.layer_index = i,
.name = layer->m_name,
.opacity = layer->m_opacity,
.visible = layer->m_visible,
.alpha_locked = layer->m_alpha_locked,
.blend_mode = layer->m_blend_mode,
});
}
const auto view = pp::app::plan_document_layer_panel_view(layer_inputs, Canvas::I->m_current_layer_idx);
if (!view)
{
LOG("Layer panel view failed: %s", view.status().message);
return;
}
m_opacity->set_value(view.value().current_opacity);
m_alpha_lock->set_value(view.value().current_alpha_locked);
m_blend_mode->set_index(view.value().current_blend_mode);
for (const auto& layer : view.value().layers)
{
if (layer.layer_index >= 0 && layer.layer_index < static_cast<int>(m_layers.size()))
m_layers[layer.layer_index]->m_visibility->set_value(layer.visible);
} }
} }

View File

@@ -1172,6 +1172,24 @@ if(TARGET pano_cli)
LABELS "app;document;integration;desktop-fast;fuzz" LABELS "app;document;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_layer_panel_view_smoke
COMMAND pano_cli plan-layer-panel-view --layer-count 3 --current-index 1 --hidden-index 2 --locked-index 1 --current-opacity 0.25 --current-blend-mode 4)
set_tests_properties(pano_cli_plan_layer_panel_view_smoke PROPERTIES
LABELS "app;document;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-layer-panel-view\".*\"layers\":3.*\"currentIndex\":1.*\"currentName\":\"Layer 1\".*\"currentOpacity\":0.25.*\"currentAlphaLocked\":true.*\"currentBlendMode\":4.*\"visibleLayers\":2.*\"lockedLayers\":1")
add_test(NAME pano_cli_plan_layer_panel_view_rejects_bad_opacity
COMMAND pano_cli plan-layer-panel-view --layer-count 2 --current-index 1 --current-opacity 1.5)
set_tests_properties(pano_cli_plan_layer_panel_view_rejects_bad_opacity PROPERTIES
LABELS "app;document;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_layer_panel_view_rejects_bad_current
COMMAND pano_cli plan-layer-panel-view --layer-count 2 --current-index 2)
set_tests_properties(pano_cli_plan_layer_panel_view_rejects_bad_current PROPERTIES
LABELS "app;document;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_layer_merge_smoke add_test(NAME pano_cli_plan_layer_merge_smoke
COMMAND pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1) COMMAND pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1)
set_tests_properties(pano_cli_plan_layer_merge_smoke PROPERTIES set_tests_properties(pano_cli_plan_layer_merge_smoke PROPERTIES

View File

@@ -3,6 +3,7 @@
#include <cmath> #include <cmath>
#include <string> #include <string>
#include <vector>
namespace { namespace {
@@ -383,6 +384,96 @@ void layer_highlight_is_transient(pp::tests::Harness& harness)
PP_EXPECT(harness, !pp::app::plan_document_layer_highlight(2, 2, true)); PP_EXPECT(harness, !pp::app::plan_document_layer_highlight(2, 2, true));
} }
void layer_panel_view_projects_current_controls_and_visibility(pp::tests::Harness& harness)
{
const std::vector<pp::app::DocumentLayerPanelInput> layers {
pp::app::DocumentLayerPanelInput {
.layer_index = 0,
.name = "Base",
.opacity = 1.0F,
.visible = true,
.alpha_locked = false,
.blend_mode = 0,
},
pp::app::DocumentLayerPanelInput {
.layer_index = 1,
.name = "Ink",
.opacity = 0.25F,
.visible = false,
.alpha_locked = true,
.blend_mode = 4,
},
};
const auto view = pp::app::plan_document_layer_panel_view(layers, 1);
PP_EXPECT(harness, view);
if (view) {
PP_EXPECT(harness, view.value().current_index == 1);
PP_EXPECT(harness, view.value().current_opacity == 0.25F);
PP_EXPECT(harness, view.value().current_alpha_locked);
PP_EXPECT(harness, view.value().current_blend_mode == 4);
PP_EXPECT(harness, view.value().layers.size() == 2);
PP_EXPECT(harness, !view.value().layers[0].current);
PP_EXPECT(harness, view.value().layers[1].current);
PP_EXPECT(harness, view.value().layers[0].name == "Base");
PP_EXPECT(harness, view.value().layers[1].name == "Ink");
PP_EXPECT(harness, view.value().layers[0].visible);
PP_EXPECT(harness, !view.value().layers[1].visible);
PP_EXPECT(harness, view.value().layers[1].alpha_locked);
}
}
void layer_panel_view_rejects_invalid_document_state(pp::tests::Harness& harness)
{
const std::vector<pp::app::DocumentLayerPanelInput> valid_layers {
pp::app::DocumentLayerPanelInput {
.layer_index = 0,
.name = "Base",
.opacity = 1.0F,
.visible = true,
.alpha_locked = false,
.blend_mode = 0,
},
};
const std::vector<pp::app::DocumentLayerPanelInput> bad_index {
pp::app::DocumentLayerPanelInput {
.layer_index = 2,
.name = "Base",
.opacity = 1.0F,
.visible = true,
.alpha_locked = false,
.blend_mode = 0,
},
};
const std::vector<pp::app::DocumentLayerPanelInput> bad_opacity {
pp::app::DocumentLayerPanelInput {
.layer_index = 0,
.name = "Base",
.opacity = std::nanf(""),
.visible = true,
.alpha_locked = false,
.blend_mode = 0,
},
};
const std::vector<pp::app::DocumentLayerPanelInput> bad_blend {
pp::app::DocumentLayerPanelInput {
.layer_index = 0,
.name = "Base",
.opacity = 1.0F,
.visible = true,
.alpha_locked = false,
.blend_mode = pp::app::document_layer_legacy_blend_mode_count,
},
};
PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view({}, 0));
PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(valid_layers, -1));
PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(valid_layers, 1));
PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(bad_index, 0));
PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(bad_opacity, 0));
PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(bad_blend, 0));
}
void layer_operation_executor_dispatches_document_mutations(pp::tests::Harness& harness) void layer_operation_executor_dispatches_document_mutations(pp::tests::Harness& harness)
{ {
FakeDocumentLayerOperationServices services; FakeDocumentLayerOperationServices services;
@@ -814,6 +905,8 @@ int main()
harness.run("layer remove keeps at least one layer", layer_remove_keeps_at_least_one_layer); harness.run("layer remove keeps at least one layer", layer_remove_keeps_at_least_one_layer);
harness.run("layer metadata plans validate values", layer_metadata_plans_validate_values); harness.run("layer metadata plans validate values", layer_metadata_plans_validate_values);
harness.run("layer highlight is transient", layer_highlight_is_transient); harness.run("layer highlight is transient", layer_highlight_is_transient);
harness.run("layer panel view projects current controls and visibility", layer_panel_view_projects_current_controls_and_visibility);
harness.run("layer panel view rejects invalid document state", layer_panel_view_rejects_invalid_document_state);
harness.run("layer operation executor dispatches document mutations", layer_operation_executor_dispatches_document_mutations); harness.run("layer operation executor dispatches document mutations", layer_operation_executor_dispatches_document_mutations);
harness.run("layer operation executor dispatches selection and metadata", layer_operation_executor_dispatches_selection_and_metadata); harness.run("layer operation executor dispatches selection and metadata", layer_operation_executor_dispatches_selection_and_metadata);
harness.run("layer operation executor preserves no op and transient actions", layer_operation_executor_preserves_no_op_and_transient_actions); harness.run("layer operation executor preserves no op and transient actions", layer_operation_executor_preserves_no_op_and_transient_actions);

View File

@@ -315,6 +315,15 @@ struct PlanLayerMenuArgs {
std::string lower_name = "Layer 0"; std::string lower_name = "Layer 0";
}; };
struct PlanLayerPanelViewArgs {
int layer_count = 2;
int current_index = 0;
int hidden_index = -1;
int locked_index = -1;
float current_opacity = 1.0F;
int current_blend_mode = 0;
};
struct PlanLayerMergeArgs { struct PlanLayerMergeArgs {
int layer_count = 2; int layer_count = 2;
int from_index = 1; int from_index = 1;
@@ -1937,6 +1946,7 @@ void print_help()
<< " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n" << " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n"
<< " plan-layer-rename --old-name NAME --new-name NAME\n" << " plan-layer-rename --old-name NAME --new-name NAME\n"
<< " plan-layer-menu --command clear|rename|merge [--no-current-layer] [--current-index N] [--animation-duration N] [--current-name NAME] [--lower-name NAME]\n" << " plan-layer-menu --command clear|rename|merge [--no-current-layer] [--current-index N] [--animation-duration N] [--current-name NAME] [--lower-name NAME]\n"
<< " plan-layer-panel-view [--layer-count N] [--current-index N] [--hidden-index N] [--locked-index N] [--current-opacity N] [--current-blend-mode N]\n"
<< " plan-layer-merge [--layer-count N] [--from-index N] [--to-index N] [--animation-duration N] [--no-history]\n" << " plan-layer-merge [--layer-count N] [--from-index N] [--to-index N] [--animation-duration N] [--no-history]\n"
<< " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n" << " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n"
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n"
@@ -4380,6 +4390,119 @@ int plan_layer_menu(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_layer_panel_view_args(
int argc,
char** argv,
PlanLayerPanelViewArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--layer-count" || key == "--current-index" || key == "--hidden-index"
|| key == "--locked-index" || key == "--current-blend-mode") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i32_arg(argv[++i]);
if (!value) {
return value.status();
}
if (key == "--layer-count") {
args.layer_count = value.value();
} else if (key == "--current-index") {
args.current_index = value.value();
} else if (key == "--hidden-index") {
args.hidden_index = value.value();
} else if (key == "--locked-index") {
args.locked_index = value.value();
} else {
args.current_blend_mode = value.value();
}
} else if (key == "--current-opacity") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_float_arg(argv[++i]);
if (!value) {
return value.status();
}
args.current_opacity = value.value();
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_layer_panel_view(int argc, char** argv)
{
PlanLayerPanelViewArgs args;
const auto status = parse_plan_layer_panel_view_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-layer-panel-view", status.message);
return 2;
}
std::vector<pp::app::DocumentLayerPanelInput> layers;
if (args.layer_count > 0) {
layers.reserve(static_cast<std::size_t>(args.layer_count));
}
for (int i = 0; i < args.layer_count; ++i) {
layers.push_back(pp::app::DocumentLayerPanelInput {
.layer_index = i,
.name = "Layer " + std::to_string(i),
.opacity = i == args.current_index ? args.current_opacity : 1.0F,
.visible = i != args.hidden_index,
.alpha_locked = i == args.locked_index,
.blend_mode = i == args.current_index ? args.current_blend_mode : 0,
});
}
const auto view = pp::app::plan_document_layer_panel_view(layers, args.current_index);
if (!view) {
print_error("plan-layer-panel-view", view.status().message);
return 2;
}
int visible_count = 0;
int locked_count = 0;
for (const auto& layer : view.value().layers) {
if (layer.visible) {
visible_count += 1;
}
if (layer.alpha_locked) {
locked_count += 1;
}
}
const auto current_layer = std::find_if(
view.value().layers.begin(),
view.value().layers.end(),
[](const pp::app::DocumentLayerPanelLayerView& layer) { return layer.current; });
if (current_layer == view.value().layers.end()) {
print_error("plan-layer-panel-view", "layer panel view did not include the current layer");
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-layer-panel-view\""
<< ",\"state\":{\"layerCount\":" << args.layer_count
<< ",\"currentIndex\":" << args.current_index
<< ",\"hiddenIndex\":" << args.hidden_index
<< ",\"lockedIndex\":" << args.locked_index
<< ",\"currentOpacity\":" << args.current_opacity
<< ",\"currentBlendMode\":" << args.current_blend_mode
<< "},\"view\":{\"layers\":" << view.value().layers.size()
<< ",\"currentIndex\":" << view.value().current_index
<< ",\"currentName\":\"" << json_escape(current_layer->name)
<< "\",\"currentOpacity\":" << view.value().current_opacity
<< ",\"currentAlphaLocked\":" << json_bool(view.value().current_alpha_locked)
<< ",\"currentBlendMode\":" << view.value().current_blend_mode
<< ",\"visibleLayers\":" << visible_count
<< ",\"lockedLayers\":" << locked_count
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_layer_merge_args( pp::foundation::Status parse_plan_layer_merge_args(
int argc, int argc,
char** argv, char** argv,
@@ -9170,6 +9293,10 @@ int main(int argc, char** argv)
return plan_layer_menu(argc, argv); return plan_layer_menu(argc, argv);
} }
if (command == "plan-layer-panel-view") {
return plan_layer_panel_view(argc, argv);
}
if (command == "plan-layer-merge") { if (command == "plan-layer-merge") {
return plan_layer_merge(argc, argv); return plan_layer_merge(argc, argv);
} }