diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index b191eef..bb47fee 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,11 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_compute` + frame planning now routes brush-quad construction, mixer feedback bounds, + 2D/3D projection selection intent, and frame assembly through the retained + stroke execution helper; legacy projection geometry, stroke samples, and live + draw execution remain retained. - 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` pad-region planning now routes through the retained stroke execution helper wrapping `pp_paint_renderer`; pad color selection, dirty-face iteration, diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 20c78ce..58a6d05 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3083,6 +3083,10 @@ Results: execution helper wrapping `pp_paint_renderer`, while pad color selection, dirty-face iteration, framebuffer copies, quad upload, and draw execution remain retained. +- `Canvas::stroke_draw_compute` frame planning now shares the retained stroke + execution helper for brush-quad construction, mixer feedback bounds, 2D/3D + projection selection intent, and frame assembly, while legacy projection + geometry, stroke samples, and live draw execution remain retained. - Remaining simple color, hue, color-quad, grid heightmap, and pen/line preview shader setup in UI nodes and canvas modes now shares retained helper surfaces, while geometry, texture/sampler binding, blend/depth state, diff --git a/src/canvas.cpp b/src/canvas.cpp index f092be9..c0bc3f2 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -587,78 +587,32 @@ glm::vec4 Canvas::stroke_draw_samples( std::vector Canvas::stroke_draw_compute(Stroke& stroke) const { - std::vector ret; - StrokeSample prev = stroke.m_prev_sample; auto samples = stroke.compute_samples(); - std::array B = { - vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} }, - vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} }, - vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} }, - vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} }, - }; - for (const auto& s : samples) - { - if (!s.valid()) - continue; - - ret.emplace_back(); - auto& f = ret.back(); - - glm::vec2 dx_mix(prev.size * 0.5f, 0), dy_mix(0, prev.size * 0.5f); - glm::vec2 off_mix[4] = { - -dx_mix - dy_mix, // A - bottom-left - -dx_mix + dy_mix, // B - top-left - +dx_mix + dy_mix, // C - top-right - +dx_mix - dy_mix, // D - bottom-right - }; - // P is the initial square centered at the cursor location - glm::vec2 dx(s.size * 0.5f, 0), dy(0, s.size * 0.5f); - glm::vec2 off[4] = { - -dx - dy, // A - bottom-left - -dx + dy, // B - top-left - +dx + dy, // C - top-right - +dx - dy, // D - bottom-right - }; - - glm::vec2 mixer_sz(App::I->width, App::I->height); - glm::vec2 mixer_bb_min(mixer_sz); - glm::vec2 mixer_bb_max(0, 0); - for (int j = 0; j < 4; j++) - { - auto p = (xy(prev.pos) + App::I->zoom * s.scale * off_mix[j] * glm::orientate2(-s.angle)); - mixer_bb_min = glm::max({ 0, 0 }, glm::min(mixer_bb_min, p)); - mixer_bb_max = glm::min(mixer_sz, glm::max(mixer_bb_max, p)); - - if (s.pos.z == 0.f) - { - B[j].pos = glm::vec4(xy(s.pos) + App::I->zoom * s.scale * off[j] * glm::orientate2(-s.angle) - glm::vec2(0, 1), 1, 1); - } - else - { - auto m = glm::inverse(glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 })); - glm::vec3 off_3d = m * glm::vec4(off[j], 0, 1); - B[j].pos = glm::vec4(s.pos + off_3d, 1.f); - } - B[j].uvs2 = p / mixer_sz; - } - - f.m_mixer_rect = glm::vec4(glm::floor(mixer_bb_min), glm::ceil(mixer_bb_max - mixer_bb_min)) / App::I->zoom; - f.col = glm::vec4(s.col, 1); - f.flow = s.flow; - f.opacity = s.opacity; - if (s.pos.z == 0.f) - { - f.shapes = stroke_draw_project(B, false, m_mv); - } - else - { - auto m = glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 }); - f.shapes = stroke_draw_project(B, true, m); - } - - prev = s; - } - return ret; + return pp::panopainter::plan_legacy_canvas_stroke_frames( + pp::panopainter::LegacyCanvasStrokeComputeRequest { + .previous_sample = stroke.m_prev_sample, + .samples = samples, + .zoom = App::I->zoom, + .mixer_size = glm::vec2(App::I->width, App::I->height), + .model_view = m_mv, + }, + [&](std::array& brush_quad, bool project_3d, glm::mat4 model_view) { + return stroke_draw_project(brush_quad, project_3d, model_view); + }, + []( + glm::vec4 mixer_rect, + glm::vec4 color, + float flow, + float opacity, + std::array, 6>&& shapes) { + return StrokeFrame { + .col = color, + .flow = flow, + .opacity = opacity, + .shapes = std::move(shapes), + .m_mixer_rect = mixer_rect, + }; + }); } void Canvas::stroke_draw() diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h index cb17a2c..0ca0320 100644 --- a/src/legacy_canvas_stroke_execution_services.h +++ b/src/legacy_canvas_stroke_execution_services.h @@ -1,13 +1,18 @@ #pragma once #include "paint_renderer/compositor.h" +#include "brush.h" #include "../libs/glm/glm/glm.hpp" +#include "../libs/glm/glm/gtc/matrix_transform.hpp" +#include "../libs/glm/glm/gtx/rotate_vector.hpp" #include "util.h" #include #include #include #include +#include +#include namespace pp::panopainter { @@ -58,6 +63,95 @@ struct LegacyCanvasStrokePadRegionResult { std::array ndc_quad {}; }; +struct LegacyCanvasStrokeComputeRequest { + StrokeSample previous_sample {}; + std::span samples; + float zoom = 1.0f; + glm::vec2 mixer_size {}; + glm::mat4 model_view { 1.0f }; +}; + +template +[[nodiscard]] auto plan_legacy_canvas_stroke_frames( + const LegacyCanvasStrokeComputeRequest& request, + ProjectStroke&& project_stroke, + MakeFrame&& make_frame) +{ + using StrokeFrame = decltype(make_frame( + std::declval(), + std::declval(), + 0.0f, + 0.0f, + project_stroke(std::declval&>(), false, request.model_view))); + + std::vector frames; + StrokeSample prev = request.previous_sample; + std::array brush_quad = { + vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} }, + vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} }, + vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} }, + vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} }, + }; + + for (const auto& sample : request.samples) { + if (!sample.valid()) { + continue; + } + + const glm::vec2 mixer_dx(prev.size * 0.5f, 0); + const glm::vec2 mixer_dy(0, prev.size * 0.5f); + const glm::vec2 mixer_offsets[4] = { + -mixer_dx - mixer_dy, + -mixer_dx + mixer_dy, + +mixer_dx + mixer_dy, + +mixer_dx - mixer_dy, + }; + + const glm::vec2 brush_dx(sample.size * 0.5f, 0); + const glm::vec2 brush_dy(0, sample.size * 0.5f); + const glm::vec2 brush_offsets[4] = { + -brush_dx - brush_dy, + -brush_dx + brush_dy, + +brush_dx + brush_dy, + +brush_dx - brush_dy, + }; + + glm::vec2 mixer_bb_min(request.mixer_size); + glm::vec2 mixer_bb_max(0, 0); + for (int i = 0; i < 4; ++i) { + const auto mixer_point = + xy(prev.pos) + request.zoom * sample.scale * mixer_offsets[i] * glm::orientate2(-sample.angle); + mixer_bb_min = glm::max(glm::vec2(0, 0), glm::min(mixer_bb_min, mixer_point)); + mixer_bb_max = glm::min(request.mixer_size, glm::max(mixer_bb_max, mixer_point)); + + if (sample.pos.z == 0.0f) { + brush_quad[i].pos = glm::vec4( + xy(sample.pos) + + request.zoom * sample.scale * brush_offsets[i] * glm::orientate2(-sample.angle) - + glm::vec2(0, 1), + 1, + 1); + } else { + const auto inverse_view = glm::inverse(glm::lookAt({ 0, 0, 0 }, sample.pos, { 0, 1, 0 })); + const glm::vec3 offset_3d = inverse_view * glm::vec4(brush_offsets[i], 0, 1); + brush_quad[i].pos = glm::vec4(sample.pos + offset_3d, 1.0f); + } + brush_quad[i].uvs2 = mixer_point / request.mixer_size; + } + + const auto mixer_rect = + glm::vec4(glm::floor(mixer_bb_min), glm::ceil(mixer_bb_max - mixer_bb_min)) / request.zoom; + auto shapes = sample.pos.z == 0.0f + ? project_stroke(brush_quad, false, request.model_view) + : project_stroke(brush_quad, true, glm::lookAt({ 0, 0, 0 }, sample.pos, { 0, 1, 0 })); + frames.push_back(make_frame(mixer_rect, glm::vec4(sample.col, 1), sample.flow, sample.opacity, std::move(shapes))); + + prev = sample; + } + + return frames; +} + [[nodiscard]] inline pp::paint_renderer::CanvasStrokeBox legacy_canvas_stroke_box(glm::vec4 box) noexcept { return pp::paint_renderer::CanvasStrokeBox {