Extract retained stroke preview setup planner
This commit is contained in:
@@ -18,6 +18,14 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
|
||||
## Recent Reductions
|
||||
|
||||
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
|
||||
now routes retained preview stroke max-size fallback, dual-preview max-size
|
||||
derivation, pattern-scale flips, and Bezier preview-point generation through
|
||||
`plan_legacy_node_stroke_preview_stroke_setup(...)`; compositor coverage now
|
||||
also locks the retained stroke-setup curve/pressure intent and
|
||||
pass-sequence validation short-circuit behavior. Brush object mutation,
|
||||
camera wiring, retained `Stroke` population, and all live GL execution
|
||||
remain in the legacy preview node.
|
||||
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` per-layer
|
||||
blend composite now routes merge-RTT unbind, shader setup, optional
|
||||
destination-copy feedback, draw, and cleanup ordering through
|
||||
|
||||
@@ -3155,6 +3155,12 @@ Results:
|
||||
dual-pass/background/main-pass/final-composite/copy-back ordering, while the
|
||||
remaining local preview ownership is concentrated around `stroke_draw_mix()`
|
||||
setup/execution.
|
||||
- `NodeStrokePreview::draw_stroke_immediate()` now routes preview stroke
|
||||
max-size fallback, dual-preview max-size derivation, pattern-scale flips,
|
||||
and Bezier preview-point generation through
|
||||
`plan_legacy_node_stroke_preview_stroke_setup(...)`, while brush mutation,
|
||||
camera wiring, retained `Stroke` population, and live GL execution remain in
|
||||
the preview node.
|
||||
- `NodeStrokePreview::stroke_draw_mix()` now shares one local helper for mixer
|
||||
framebuffer bind/unbind, viewport/scissor/blend state, texture-slot
|
||||
binding, and final plane draw, while material planning and shader uniform
|
||||
|
||||
@@ -509,6 +509,16 @@ Done Checks:
|
||||
|
||||
Progress Notes:
|
||||
|
||||
- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` now routes preview
|
||||
stroke max-size fallback, dual-preview max-size derivation, pattern-scale
|
||||
flips, and Bezier preview-point generation through
|
||||
`plan_legacy_node_stroke_preview_stroke_setup(...)`; compositor coverage now
|
||||
also locks the retained stroke-setup curve/pressure intent and
|
||||
pass-sequence request-validation short-circuit behavior. The preview node
|
||||
still owns brush object mutation, camera wiring, retained `Stroke`
|
||||
population, and live GL execution. Next slice should target the remaining
|
||||
higher-level preview pass orchestration seam without reopening landed
|
||||
sample, mix, pass-sequence, or final-composite helpers.
|
||||
- 2026-06-13: `Canvas::draw_merge()` per-layer blend composite now routes
|
||||
merge-RTT unbind, shader setup, optional destination-copy feedback, draw,
|
||||
and cleanup ordering through
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include "paint_renderer/compositor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
@@ -222,4 +224,98 @@ struct LegacyNodeStrokePreviewPassSequenceRequest {
|
||||
return true;
|
||||
}
|
||||
|
||||
struct LegacyNodeStrokePreviewStrokePoint {
|
||||
glm::vec3 position {};
|
||||
float pressure = 0.0f;
|
||||
};
|
||||
|
||||
struct LegacyNodeStrokePreviewStrokeSetupPlan {
|
||||
float stroke_max_size = 0.0f;
|
||||
float dual_stroke_max_size = 0.0f;
|
||||
bool dual_enabled = false;
|
||||
glm::vec2 pattern_scale {};
|
||||
std::vector<LegacyNodeStrokePreviewStrokePoint> points;
|
||||
};
|
||||
|
||||
struct LegacyNodeStrokePreviewStrokeSetupRequest {
|
||||
glm::vec2 preview_size {};
|
||||
float zoom = 1.0f;
|
||||
float brush_tip_size = 0.0f;
|
||||
float stroke_max_size_override = 0.0f;
|
||||
float pad_override = NAN;
|
||||
bool tip_size_pressure = false;
|
||||
bool dual_enabled = false;
|
||||
float dual_size = 1.0f;
|
||||
float pattern_scale = 0.0f;
|
||||
bool pattern_flipx = false;
|
||||
bool pattern_flipy = false;
|
||||
int preview_point_count = 100;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline glm::vec2 evaluate_legacy_node_stroke_preview_bezier(
|
||||
std::vector<glm::vec2> control_points,
|
||||
float t) noexcept
|
||||
{
|
||||
if (control_points.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::size_t remaining = control_points.size(); remaining > 1; --remaining) {
|
||||
for (std::size_t i = 0; i + 1 < remaining; ++i) {
|
||||
control_points[i] = glm::mix(control_points[i], control_points[i + 1], t);
|
||||
}
|
||||
}
|
||||
|
||||
return control_points.front();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline LegacyNodeStrokePreviewStrokeSetupPlan plan_legacy_node_stroke_preview_stroke_setup(
|
||||
const LegacyNodeStrokePreviewStrokeSetupRequest& request) noexcept
|
||||
{
|
||||
LegacyNodeStrokePreviewStrokeSetupPlan plan;
|
||||
plan.stroke_max_size = request.stroke_max_size_override > 0.0f ?
|
||||
request.stroke_max_size_override :
|
||||
request.preview_size.y * 0.75f;
|
||||
plan.dual_enabled = request.dual_enabled;
|
||||
plan.dual_stroke_max_size = plan.stroke_max_size * request.dual_size;
|
||||
plan.pattern_scale = glm::vec2(request.pattern_scale);
|
||||
if (request.pattern_flipx) {
|
||||
plan.pattern_scale.x *= -1.0f;
|
||||
}
|
||||
if (request.pattern_flipy) {
|
||||
plan.pattern_scale.y *= -1.0f;
|
||||
}
|
||||
|
||||
const float min_pad = request.preview_size.x * 0.05f;
|
||||
float pad = (5.0f + glm::max(glm::min(plan.stroke_max_size, request.brush_tip_size) / 2.0f, min_pad)) * request.zoom;
|
||||
if (request.tip_size_pressure) {
|
||||
pad = min_pad * request.zoom;
|
||||
}
|
||||
if (!std::isnan(request.pad_override)) {
|
||||
pad = request.pad_override;
|
||||
}
|
||||
|
||||
const float width = request.preview_size.x * request.zoom;
|
||||
const float height = request.preview_size.y * request.zoom;
|
||||
const std::vector<glm::vec2> keypoints {
|
||||
{ pad, height / 2.0f },
|
||||
{ width / 2.0f, 0.0f },
|
||||
{ width / 2.0f, height },
|
||||
{ width - pad, height / 2.0f },
|
||||
};
|
||||
|
||||
const int point_count = std::max(request.preview_point_count, 0);
|
||||
plan.points.reserve(static_cast<std::size_t>(point_count));
|
||||
for (int i = 0; i < point_count; ++i) {
|
||||
const float t = static_cast<float>(i) / static_cast<float>(point_count);
|
||||
const float pressure = glm::clamp((1.0f - glm::abs(t * 2.0f - 1.0f)) * 1.1f, 0.0f, 1.0f);
|
||||
plan.points.push_back(LegacyNodeStrokePreviewStrokePoint {
|
||||
.position = glm::vec3(evaluate_legacy_node_stroke_preview_bezier(keypoints, t), 0.0f),
|
||||
.pressure = pressure,
|
||||
});
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
|
||||
@@ -687,6 +687,20 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
float zoom = root()->m_zoom;
|
||||
|
||||
glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() };
|
||||
const auto stroke_setup = pp::panopainter::plan_legacy_node_stroke_preview_stroke_setup(
|
||||
pp::panopainter::LegacyNodeStrokePreviewStrokeSetupRequest {
|
||||
.preview_size = m_size,
|
||||
.zoom = zoom,
|
||||
.brush_tip_size = m_brush->m_tip_size,
|
||||
.stroke_max_size_override = m_max_size,
|
||||
.pad_override = m_pad_override,
|
||||
.tip_size_pressure = m_brush->m_tip_size_pressure,
|
||||
.dual_enabled = m_brush->m_dual_enabled,
|
||||
.dual_size = m_brush->m_dual_size,
|
||||
.pattern_scale = m_brush->m_pattern_scale,
|
||||
.pattern_flipx = m_brush->m_pattern_flipx,
|
||||
.pattern_flipy = m_brush->m_pattern_flipy,
|
||||
});
|
||||
glm::mat4 ortho_proj = glm::ortho<float>(0, size.x, 0, size.y, -1, 1);
|
||||
apply_stroke_preview_viewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
|
||||
m_rtt.bindFramebuffer();
|
||||
@@ -702,7 +716,7 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
Stroke m_dual_stroke;
|
||||
|
||||
m_stroke.m_filter_points = false;
|
||||
m_stroke.m_max_size = m_max_size > 0 ? m_max_size : m_size.y * .75f;
|
||||
m_stroke.m_max_size = stroke_setup.stroke_max_size;
|
||||
m_stroke.m_camera.fov = Canvas::I->m_cam_fov;
|
||||
m_stroke.m_camera.rot = Canvas::I->m_cam_rot;
|
||||
m_stroke.reset(true);
|
||||
@@ -727,44 +741,24 @@ void NodeStrokePreview::draw_stroke_immediate()
|
||||
dual_brush->m_tip_texture = b->m_dual_texture;
|
||||
dual_brush->m_tip_aspect = b->m_dual_aspect;
|
||||
|
||||
if (b->m_dual_enabled)
|
||||
if (stroke_setup.dual_enabled)
|
||||
{
|
||||
m_dual_stroke.m_filter_points = false;
|
||||
m_dual_stroke.m_max_size = m_stroke.m_max_size * b->m_dual_size;
|
||||
m_dual_stroke.m_max_size = stroke_setup.dual_stroke_max_size;
|
||||
m_dual_stroke.m_camera.fov = Canvas::I->m_cam_fov;
|
||||
m_dual_stroke.m_camera.rot = Canvas::I->m_cam_rot;
|
||||
m_dual_stroke.reset(true);
|
||||
m_dual_stroke.start(dual_brush);
|
||||
}
|
||||
|
||||
for (const auto& point : stroke_setup.points)
|
||||
{
|
||||
float min_pad = size.x * 0.05f;
|
||||
float pad = (5.f + glm::max(glm::min(m_stroke.m_max_size, m_brush->m_tip_size) / 2.f, min_pad)) * zoom;
|
||||
if (b->m_tip_size_pressure)
|
||||
pad = min_pad * zoom;
|
||||
if (!glm::isnan(m_pad_override))
|
||||
pad = m_pad_override;
|
||||
float w = m_size.x * zoom;
|
||||
float h = m_size.y * zoom;
|
||||
std::vector<glm::vec2> kp = {
|
||||
{ pad, h / 2.f },
|
||||
{ w / 2.f, 0 },
|
||||
{ w / 2.f, h },
|
||||
{ w - pad, h / 2.f },
|
||||
};
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
float t = (float)i / 100.f;
|
||||
float p = glm::clamp((1.f - glm::abs(t * 2.f - 1.f)) * 1.1f, 0.f, 1.f);
|
||||
m_stroke.add_point(glm::vec3(BezierCurve::Bezier2D(kp, t), 0), p);
|
||||
if (b->m_dual_enabled)
|
||||
m_dual_stroke.add_point(glm::vec3(BezierCurve::Bezier2D(kp, t), 0), p);
|
||||
}
|
||||
m_stroke.add_point(point.position, point.pressure);
|
||||
if (stroke_setup.dual_enabled)
|
||||
m_dual_stroke.add_point(point.position, point.pressure);
|
||||
}
|
||||
|
||||
glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
|
||||
if (b->m_pattern_flipx) patt_scale.x *= -1.f;
|
||||
if (b->m_pattern_flipy) patt_scale.y *= -1.f;
|
||||
const glm::vec2 patt_scale = stroke_setup.pattern_scale;
|
||||
|
||||
apply_stroke_preview_capability(pp::renderer::gl::blend_state(), false);
|
||||
const auto stroke_feedback = stroke_preview_destination_feedback_plan(m_rtt.getWidth(), m_rtt.getHeight());
|
||||
|
||||
@@ -2421,12 +2421,99 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_
|
||||
PP_EXPECT(h, !missing_dual_prepare);
|
||||
PP_EXPECT(h, steps.empty());
|
||||
|
||||
steps.clear();
|
||||
const bool missing_main_prepare =
|
||||
pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
|
||||
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {
|
||||
.dual_pass_enabled = true,
|
||||
.prepare_dual_pass = [&] {
|
||||
steps.emplace_back("prepare_dual");
|
||||
},
|
||||
.execute_dual_pass = [&] {
|
||||
steps.emplace_back("execute_dual");
|
||||
},
|
||||
.capture_background = [&] {
|
||||
steps.emplace_back("capture_background");
|
||||
},
|
||||
.prepare_main_pass = {},
|
||||
.execute_main_pass = [&] {
|
||||
steps.emplace_back("execute_main");
|
||||
},
|
||||
.finish_main_pass = [&] {
|
||||
steps.emplace_back("finish_main");
|
||||
},
|
||||
.execute_final_composite = [&] {
|
||||
steps.emplace_back("execute_composite");
|
||||
},
|
||||
.copy_preview_result = [&] {
|
||||
steps.emplace_back("copy_preview");
|
||||
},
|
||||
});
|
||||
PP_EXPECT(h, !missing_main_prepare);
|
||||
PP_EXPECT(h, steps.empty());
|
||||
|
||||
const bool missing_required =
|
||||
pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
|
||||
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {});
|
||||
PP_EXPECT(h, !missing_required);
|
||||
}
|
||||
|
||||
void legacy_node_stroke_preview_stroke_setup_plan_preserves_curve_and_dual_inputs(pp::tests::Harness& h)
|
||||
{
|
||||
const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_stroke_setup(
|
||||
pp::panopainter::LegacyNodeStrokePreviewStrokeSetupRequest {
|
||||
.preview_size = glm::vec2(120.0F, 80.0F),
|
||||
.zoom = 2.0F,
|
||||
.brush_tip_size = 30.0F,
|
||||
.stroke_max_size_override = 0.0F,
|
||||
.pad_override = 17.0F,
|
||||
.tip_size_pressure = false,
|
||||
.dual_enabled = true,
|
||||
.dual_size = 0.25F,
|
||||
.pattern_scale = 0.5F,
|
||||
.pattern_flipx = true,
|
||||
.pattern_flipy = false,
|
||||
.preview_point_count = 4,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, near(plan.stroke_max_size, 60.0F));
|
||||
PP_EXPECT(h, near(plan.dual_stroke_max_size, 15.0F));
|
||||
PP_EXPECT(h, plan.dual_enabled);
|
||||
PP_EXPECT(h, near(plan.pattern_scale, glm::vec2(-0.5F, 0.5F)));
|
||||
PP_EXPECT(h, plan.points.size() == 4U);
|
||||
|
||||
PP_EXPECT(h, near(plan.points[0].position.x, 17.0F));
|
||||
PP_EXPECT(h, near(plan.points[0].position.y, 80.0F));
|
||||
PP_EXPECT(h, near(plan.points[0].position.z, 0.0F));
|
||||
PP_EXPECT(h, near(plan.points[0].pressure, 0.0F));
|
||||
|
||||
PP_EXPECT(h, near(plan.points[1].pressure, 0.55F));
|
||||
PP_EXPECT(h, near(plan.points[2].pressure, 1.0F));
|
||||
PP_EXPECT(h, near(plan.points[3].pressure, 0.55F));
|
||||
|
||||
const auto pressure_fallback = pp::panopainter::plan_legacy_node_stroke_preview_stroke_setup(
|
||||
pp::panopainter::LegacyNodeStrokePreviewStrokeSetupRequest {
|
||||
.preview_size = glm::vec2(120.0F, 80.0F),
|
||||
.zoom = 2.0F,
|
||||
.brush_tip_size = 30.0F,
|
||||
.stroke_max_size_override = 20.0F,
|
||||
.pad_override = NAN,
|
||||
.tip_size_pressure = true,
|
||||
.dual_enabled = false,
|
||||
.dual_size = 2.0F,
|
||||
.pattern_scale = 0.25F,
|
||||
.pattern_flipx = false,
|
||||
.pattern_flipy = true,
|
||||
.preview_point_count = 0,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, near(pressure_fallback.stroke_max_size, 20.0F));
|
||||
PP_EXPECT(h, near(pressure_fallback.dual_stroke_max_size, 40.0F));
|
||||
PP_EXPECT(h, !pressure_fallback.dual_enabled);
|
||||
PP_EXPECT(h, near(pressure_fallback.pattern_scale, glm::vec2(0.25F, -0.25F)));
|
||||
PP_EXPECT(h, pressure_fallback.points.empty());
|
||||
}
|
||||
|
||||
void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
|
||||
{
|
||||
const std::vector<int> normal_layers { 0, 0, 0 };
|
||||
@@ -2887,6 +2974,9 @@ int main()
|
||||
harness.run(
|
||||
"legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order",
|
||||
legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order);
|
||||
harness.run(
|
||||
"legacy_node_stroke_preview_stroke_setup_plan_preserves_curve_and_dual_inputs",
|
||||
legacy_node_stroke_preview_stroke_setup_plan_preserves_curve_and_dual_inputs);
|
||||
harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices);
|
||||
harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
|
||||
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
|
||||
|
||||
Reference in New Issue
Block a user