Route onion frame planning through app core

This commit is contained in:
2026-06-05 00:19:12 +02:00
parent 2feeffd6c8
commit 59210c28ea
6 changed files with 132 additions and 16 deletions

View File

@@ -246,9 +246,11 @@ Known local toolchain state:
- `src/legacy_document_animation_services.*` is the current UI-shell bridge for
animation frame commands, timeline/selected-frame execution, playback ticks,
onion-size updates, and play-mode toggles. It keeps those live paths on the
`pp_app_core` contracts while legacy `Canvas`/`Layer` frame state, canvas
mode, animation-panel timeline/playback fields, and the temporary
`NodePanelAnimation` friend adapter remain tracked by `DEBT-0022`.
`pp_app_core` contracts. `NodeCanvas` onion-skin panorama drawing now also
consumes the tested `pp_app_core` onion frame range and alpha falloff helper,
while legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel
timeline/playback fields, and the temporary `NodePanelAnimation` friend
adapter remain tracked by `DEBT-0022`.
- `src/legacy_brush_ui_services.*` is the current UI-shell bridge for brush
color, texture, preset, stroke-refresh, brush texture-list, and stroke-control
execution. `NodePanelBrushPreset` now consumes `pp_app_core` preset-list

View File

@@ -35,6 +35,11 @@ agent or engineer to remove them without reconstructing context from chat.
blocker matrix without requiring an app build first, and
`panopainter_package_smoke_readiness_self_test` guards package-kind parity
across both wrappers. Package target migration remains open.
- 2026-06-05: DEBT-0022 was narrowed. `pp_app_core` now owns tested
onion-skin frame range and alpha falloff planning, and live `NodeCanvas`
panorama drawing consumes that helper instead of open-coding frame clamping
and opacity falloff in the render loop. Legacy canvas/layer/UI execution
remains open under DEBT-0022.
- 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

View File

@@ -527,6 +527,8 @@ before the legacy layer-panel merge adapter continues.
frame add, duplicate, remove, duration adjustment, timeline moves, timeline
goto/next/previous, onion-size updates, frame selection, no-reload playback
stepping, and play-mode toggles used by the live animation panel.
`pp_app_core` also owns onion-skin frame range and alpha falloff planning now
consumed by live `NodeCanvas` panorama drawing.
`pano_cli plan-animation-panel-action` exposes the higher-level animation panel
state/action planner for goto, next, previous, playback-step, and play-toggle
automation without requiring the legacy UI or canvas.
@@ -1694,8 +1696,9 @@ Results:
goto/next/previous wrapping, onion-size rejection, service dispatch ordering,
frame-click selection planning, no-reload playback step planning,
playback toggle start/stop planning, animation panel action planning,
invalid panel timeline state rejection, non-mutating duration no-ops, and
malformed execution payload rejection.
invalid panel timeline state rejection, non-mutating duration no-ops, tested
onion-skin frame range/alpha falloff planning consumed by live `NodeCanvas`
panorama drawing, and malformed execution payload rejection.
- `pano_cli_plan_animation_operation_add_smoke`,
`pano_cli_plan_animation_operation_duration_floor_smoke`,
`pano_cli_plan_animation_operation_next_wrap_smoke`,

View File

@@ -62,6 +62,14 @@ struct DocumentAnimationOperationPlan {
bool resets_playback_timer = false;
};
struct DocumentAnimationOnionFrameRange {
int frame_count = 1;
int current_frame = 0;
int onion_size = 0;
int first_frame = 0;
int last_frame = 0;
};
class DocumentAnimationServices {
public:
virtual ~DocumentAnimationServices() = default;
@@ -122,6 +130,56 @@ public:
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
int frame_count,
int current_frame,
int onion_size)
{
const auto index_status = validate_animation_frame_index(frame_count, current_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(index_status);
}
if (onion_size < 0) {
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
}
const auto first = std::max<std::int64_t>(
static_cast<std::int64_t>(current_frame) - static_cast<std::int64_t>(onion_size),
0);
const auto last = std::min<std::int64_t>(
static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(onion_size),
static_cast<std::int64_t>(frame_count) - 1);
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::success(
DocumentAnimationOnionFrameRange {
.frame_count = frame_count,
.current_frame = current_frame,
.onion_size = onion_size,
.first_frame = static_cast<int>(first),
.last_frame = static_cast<int>(last),
});
}
[[nodiscard]] inline float animation_onion_frame_alpha(
const DocumentAnimationOnionFrameRange& range,
int frame) noexcept
{
if (frame < range.first_frame || frame > range.last_frame) {
return 0.0f;
}
const int distance = frame >= range.current_frame
? frame - range.current_frame
: range.current_frame - frame;
if (distance > range.onion_size) {
return 0.0f;
}
return 1.0f - static_cast<float>(distance) / static_cast<float>(range.onion_size + 1);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
int frame_count,
int current_frame)

View File

@@ -8,6 +8,7 @@
#include "app_core/canvas_hotkey.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_animation.h"
#include "app.h"
#include "legacy_canvas_tool_services.h"
#include "legacy_history_services.h"
@@ -514,12 +515,17 @@ void NodeCanvas::draw()
auto layer_index = i;
for (int plane_index = 0; plane_index < 6; plane_index++)
{
int onion_size = App::I->animation->get_onion_size();
int frame_current = m_canvas->m_layers[layer_index]->m_frame_index;
int frame_start = glm::max<int>(frame_current - onion_size, 0);
int frame_end = glm::min<int>(frame_current + onion_size, m_canvas->m_layers[layer_index]->frames_count() - 1);
const auto onion_range_result = pp::app::plan_animation_onion_frame_range(
m_canvas->m_layers[layer_index]->frames_count(),
m_canvas->m_layers[layer_index]->m_frame_index,
App::I->animation->get_onion_size());
if (!onion_range_result) {
LOG("NodeCanvas onion frame range failed: %s", onion_range_result.status().message);
continue;
}
const auto onion_range = onion_range_result.value();
bool faces = false;
for (int frame = frame_start; frame <= frame_end; frame++)
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
faces |= m_canvas->m_layers[layer_index]->face(plane_index, frame);
if (!(m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index) &&
(!m_canvas->m_layers[layer_index]->m_visible ||
@@ -557,9 +563,9 @@ void NodeCanvas::draw()
m_canvas->m_tmp[plane_index].bindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
for (int frame = frame_start; frame <= frame_end; frame++)
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
{
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
const float onion_alpha = pp::app::animation_onion_frame_alpha(onion_range, frame);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity* onion_alpha);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
@@ -619,9 +625,9 @@ void NodeCanvas::draw()
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
unbind_texture_2d();
for (int frame = frame_start; frame <= frame_end; frame++)
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
{
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
const float onion_alpha = pp::app::animation_onion_frame_alpha(onion_range, frame);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
@@ -645,9 +651,9 @@ void NodeCanvas::draw()
ShaderManager::u_int(kShaderUniform::Highlight, m_canvas->m_layers[layer_index]->m_hightlight);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
for (int frame = frame_start; frame <= frame_end; frame++)
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
{
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
const float onion_alpha = pp::app::animation_onion_frame_alpha(onion_range, frame);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();

View File

@@ -426,6 +426,46 @@ void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& har
PP_EXPECT(harness, !pp::app::plan_animation_onion_size(-1));
}
void onion_frame_ranges_clamp_edges_and_alpha(pp::tests::Harness& harness)
{
const auto center = pp::app::plan_animation_onion_frame_range(5, 2, 1);
PP_REQUIRE(harness, center);
PP_EXPECT(harness, center.value().first_frame == 1);
PP_EXPECT(harness, center.value().last_frame == 3);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 2) == 1.0f);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 1) == 0.5f);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 3) == 0.5f);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 0) == 0.0f);
const auto left = pp::app::plan_animation_onion_frame_range(5, 0, 3);
PP_REQUIRE(harness, left);
PP_EXPECT(harness, left.value().first_frame == 0);
PP_EXPECT(harness, left.value().last_frame == 3);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(left.value(), 0) == 1.0f);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(left.value(), 3) == 0.25f);
const auto right = pp::app::plan_animation_onion_frame_range(5, 4, 3);
PP_REQUIRE(harness, right);
PP_EXPECT(harness, right.value().first_frame == 1);
PP_EXPECT(harness, right.value().last_frame == 4);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(right.value(), 1) == 0.25f);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(right.value(), 4) == 1.0f);
const auto single = pp::app::plan_animation_onion_frame_range(1, 0, 8);
PP_REQUIRE(harness, single);
PP_EXPECT(harness, single.value().first_frame == 0);
PP_EXPECT(harness, single.value().last_frame == 0);
PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(single.value(), 0) == 1.0f);
}
void onion_frame_ranges_reject_invalid_inputs(pp::tests::Harness& harness)
{
PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(0, 0, 1));
PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, -1, 1));
PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, 3, 1));
PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, 1, -1));
}
void executor_dispatches_mutating_frame_operations(pp::tests::Harness& harness)
{
FakeDocumentAnimationServices services;
@@ -590,6 +630,8 @@ int main()
harness.run("panel actions plan timeline and playback intent", panel_actions_plan_timeline_and_playback_intent);
harness.run("panel actions reject invalid timeline state", panel_actions_reject_invalid_timeline_state);
harness.run("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation);
harness.run("onion frame ranges clamp edges and alpha", onion_frame_ranges_clamp_edges_and_alpha);
harness.run("onion frame ranges reject invalid inputs", onion_frame_ranges_reject_invalid_inputs);
harness.run("executor dispatches mutating frame operations", executor_dispatches_mutating_frame_operations);
harness.run("executor dispatches timeline and parameter operations", executor_dispatches_timeline_and_parameter_operations);
harness.run("executor rejects malformed animation plans", executor_rejects_malformed_animation_plans);