Route stroke frame planning through helper

This commit is contained in:
2026-06-13 06:39:16 +02:00
parent b8c6e11f41
commit 6251c6d566
4 changed files with 128 additions and 71 deletions

View File

@@ -18,6 +18,11 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions ## 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 - 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` pad-region
planning now routes through the retained stroke execution helper wrapping planning now routes through the retained stroke execution helper wrapping
`pp_paint_renderer`; pad color selection, dirty-face iteration, `pp_paint_renderer`; pad color selection, dirty-face iteration,

View File

@@ -3083,6 +3083,10 @@ Results:
execution helper wrapping `pp_paint_renderer`, while pad color selection, execution helper wrapping `pp_paint_renderer`, while pad color selection,
dirty-face iteration, framebuffer copies, quad upload, and draw execution dirty-face iteration, framebuffer copies, quad upload, and draw execution
remain retained. 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 - Remaining simple color, hue, color-quad, grid heightmap, and pen/line
preview shader setup in UI nodes and canvas modes now shares retained helper preview shader setup in UI nodes and canvas modes now shares retained helper
surfaces, while geometry, texture/sampler binding, blend/depth state, surfaces, while geometry, texture/sampler binding, blend/depth state,

View File

@@ -587,78 +587,32 @@ glm::vec4 Canvas::stroke_draw_samples(
std::vector<Canvas::StrokeFrame> Canvas::stroke_draw_compute(Stroke& stroke) const std::vector<Canvas::StrokeFrame> Canvas::stroke_draw_compute(Stroke& stroke) const
{ {
std::vector<StrokeFrame> ret;
StrokeSample prev = stroke.m_prev_sample;
auto samples = stroke.compute_samples(); auto samples = stroke.compute_samples();
std::array<vertex_t, 4> B = { return pp::panopainter::plan_legacy_canvas_stroke_frames(
vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} }, pp::panopainter::LegacyCanvasStrokeComputeRequest {
vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} }, .previous_sample = stroke.m_prev_sample,
vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} }, .samples = samples,
vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} }, .zoom = App::I->zoom,
}; .mixer_size = glm::vec2(App::I->width, App::I->height),
for (const auto& s : samples) .model_view = m_mv,
{ },
if (!s.valid()) [&](std::array<vertex_t, 4>& brush_quad, bool project_3d, glm::mat4 model_view) {
continue; return stroke_draw_project(brush_quad, project_3d, model_view);
},
ret.emplace_back(); [](
auto& f = ret.back(); glm::vec4 mixer_rect,
glm::vec4 color,
glm::vec2 dx_mix(prev.size * 0.5f, 0), dy_mix(0, prev.size * 0.5f); float flow,
glm::vec2 off_mix[4] = { float opacity,
-dx_mix - dy_mix, // A - bottom-left std::array<std::vector<vertex_t>, 6>&& shapes) {
-dx_mix + dy_mix, // B - top-left return StrokeFrame {
+dx_mix + dy_mix, // C - top-right .col = color,
+dx_mix - dy_mix, // D - bottom-right .flow = flow,
}; .opacity = opacity,
// P is the initial square centered at the cursor location .shapes = std::move(shapes),
glm::vec2 dx(s.size * 0.5f, 0), dy(0, s.size * 0.5f); .m_mixer_rect = mixer_rect,
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;
} }
void Canvas::stroke_draw() void Canvas::stroke_draw()

View File

@@ -1,13 +1,18 @@
#pragma once #pragma once
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
#include "brush.h"
#include "../libs/glm/glm/glm.hpp" #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 "util.h"
#include <array> #include <array>
#include <functional> #include <functional>
#include <span> #include <span>
#include <string_view> #include <string_view>
#include <utility>
#include <vector>
namespace pp::panopainter { namespace pp::panopainter {
@@ -58,6 +63,95 @@ struct LegacyCanvasStrokePadRegionResult {
std::array<pp::paint_renderer::CanvasStrokePoint, 6> ndc_quad {}; std::array<pp::paint_renderer::CanvasStrokePoint, 6> ndc_quad {};
}; };
struct LegacyCanvasStrokeComputeRequest {
StrokeSample previous_sample {};
std::span<const StrokeSample> samples;
float zoom = 1.0f;
glm::vec2 mixer_size {};
glm::mat4 model_view { 1.0f };
};
template <typename ProjectStroke, typename MakeFrame>
[[nodiscard]] auto plan_legacy_canvas_stroke_frames(
const LegacyCanvasStrokeComputeRequest& request,
ProjectStroke&& project_stroke,
MakeFrame&& make_frame)
{
using StrokeFrame = decltype(make_frame(
std::declval<glm::vec4>(),
std::declval<glm::vec4>(),
0.0f,
0.0f,
project_stroke(std::declval<std::array<vertex_t, 4>&>(), false, request.model_view)));
std::vector<StrokeFrame> frames;
StrokeSample prev = request.previous_sample;
std::array<vertex_t, 4> 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 [[nodiscard]] inline pp::paint_renderer::CanvasStrokeBox legacy_canvas_stroke_box(glm::vec4 box) noexcept
{ {
return pp::paint_renderer::CanvasStrokeBox { return pp::paint_renderer::CanvasStrokeBox {