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
legacy `Canvas`, `NodeLayer`, `NodePanelLayer`, and `ActionManager`
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
menu routing, export-menu routing, main-toolbar commands, About menu
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
`pano_cli plan-animation-panel-view`, including stale-selection behavior.
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,
thumbnail, and object-draw history paths now query saved blend state through
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-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-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-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 |

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
operations now dispatch through `DocumentLayerOperationServices` before the
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,
rename, and merge-down labels/actions, and direct Layer menu commands now
dispatch through `DocumentLayerMenuServices` before the legacy canvas/layer UI

View File

@@ -8,6 +8,7 @@
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace pp::app {
@@ -84,6 +85,33 @@ struct DocumentLayerMergePlan {
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 {
public:
virtual ~DocumentLayerMenuServices() = default;
@@ -158,6 +186,60 @@ public:
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(
std::string_view old_name,
std::string_view requested_name)

View File

@@ -1,4 +1,5 @@
#include "pch.h"
#include "app_core/document_layer.h"
#include "log.h"
#include "node_panel_layer.h"
#include "canvas.h"
@@ -367,13 +368,38 @@ void NodePanelLayer::clear()
void NodePanelLayer::update_attributes()
{
auto& l = Canvas::I->m_layers[Canvas::I->m_current_layer_idx];
m_opacity->set_value(l->m_opacity);
m_alpha_lock->set_value(l->m_alpha_locked);
m_blend_mode->set_index(l->m_blend_mode);
for (int i = 0; i < Canvas::I->m_layers.size(); i++)
if (!Canvas::I)
return;
std::vector<pp::app::DocumentLayerPanelInput> layer_inputs;
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"
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
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

View File

@@ -3,6 +3,7 @@
#include <cmath>
#include <string>
#include <vector>
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));
}
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)
{
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 metadata plans validate values", layer_metadata_plans_validate_values);
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 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);

View File

@@ -315,6 +315,15 @@ struct PlanLayerMenuArgs {
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 {
int layer_count = 2;
int from_index = 1;
@@ -1937,6 +1946,7 @@ void print_help()
<< " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\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-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-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"
@@ -4380,6 +4390,119 @@ int plan_layer_menu(int argc, char** argv)
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(
int argc,
char** argv,
@@ -9170,6 +9293,10 @@ int main(int argc, char** 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") {
return plan_layer_merge(argc, argv);
}