Extract stroke dirty bounds planning
This commit is contained in:
@@ -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. Stroke sample copy bounds, live
|
||||||
|
face dirty-box accumulation, and preview padding region math now live as
|
||||||
|
tested `pp_paint_renderer` planners and are consumed by retained Canvas and
|
||||||
|
stroke sample execution adapters. Canvas/preview still own GL ordering,
|
||||||
|
RTT/texture binding, history mutation, and final dirty state storage.
|
||||||
- 2026-06-12: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now reuses
|
- 2026-06-12: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now reuses
|
||||||
`legacy_canvas_stroke_composite_services.h` for commit-time final stroke
|
`legacy_canvas_stroke_composite_services.h` for commit-time final stroke
|
||||||
`kShader::CompDraw` binding and composite/pattern/dual uniform writes.
|
`kShader::CompDraw` binding and composite/pattern/dual uniform writes.
|
||||||
|
|||||||
@@ -1354,8 +1354,11 @@ stroke composite `kShader::CompDraw` setup now pass through
|
|||||||
reuses that same composite service for commit-time final stroke compositing.
|
reuses that same composite service for commit-time final stroke compositing.
|
||||||
Stroke padding and commit dilate `kShader::StrokePad`/`kShader::StrokeDilate`
|
Stroke padding and commit dilate `kShader::StrokePad`/`kShader::StrokeDilate`
|
||||||
setup now pass through `legacy_canvas_stroke_edge_services.h`, leaving
|
setup now pass through `legacy_canvas_stroke_edge_services.h`, leaving
|
||||||
RTT/texture ownership, dirty-box policy, checkerboard/non-stroke composite
|
RTT/texture ownership, checkerboard/non-stroke composite shaders, and retained
|
||||||
shaders, quad expansion, and retained callback execution under `DEBT-0036`.
|
callback execution under `DEBT-0036`. Stroke sample copy bounds, live face
|
||||||
|
dirty-box accumulation, and preview padding region math now live as tested
|
||||||
|
`pp_paint_renderer` planners while retained Canvas/preview code still owns GL
|
||||||
|
ordering, RTT/texture binding, history mutation, and final dirty state storage.
|
||||||
It also owns renderer API texture-format to
|
It also owns renderer API texture-format to
|
||||||
OpenGL internal/pixel/component token mapping, including depth-stencil formats,
|
OpenGL internal/pixel/component token mapping, including depth-stencil formats,
|
||||||
for future backend texture objects. `Texture2D` 2D texture binding, upload,
|
for future backend texture objects. `Texture2D` 2D texture binding, upload,
|
||||||
|
|||||||
@@ -84,6 +84,29 @@ pp::paint_renderer::CanvasStrokeMaterialPlan canvas_stroke_material_plan(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::renderer::Extent2D canvas_stroke_extent(int width, int height) noexcept
|
||||||
|
{
|
||||||
|
return pp::renderer::Extent2D {
|
||||||
|
.width = static_cast<std::uint32_t>(std::max(width, 0)),
|
||||||
|
.height = static_cast<std::uint32_t>(std::max(height, 0)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::paint_renderer::CanvasStrokeBox canvas_stroke_box(glm::vec4 box) noexcept
|
||||||
|
{
|
||||||
|
return pp::paint_renderer::CanvasStrokeBox {
|
||||||
|
.min_x = box.x,
|
||||||
|
.min_y = box.y,
|
||||||
|
.max_x = box.z,
|
||||||
|
.max_y = box.w,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec4 glm_box(pp::paint_renderer::CanvasStrokeBox box) noexcept
|
||||||
|
{
|
||||||
|
return glm::vec4(box.min_x, box.min_y, box.max_x, box.max_y);
|
||||||
|
}
|
||||||
|
|
||||||
pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
|
pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -530,11 +553,21 @@ glm::vec4 Canvas::stroke_draw_samples(
|
|||||||
P = triangulate_simple(P);
|
P = triangulate_simple(P);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<pp::paint_renderer::CanvasStrokePoint> sample_points;
|
||||||
|
sample_points.reserve(P.size());
|
||||||
|
for (const auto& vertex : P) {
|
||||||
|
sample_points.push_back(pp::paint_renderer::CanvasStrokePoint {
|
||||||
|
.x = vertex.pos.x,
|
||||||
|
.y = vertex.pos.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
|
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
|
||||||
pp::panopainter::LegacyStrokeSampleExecutionRequest {
|
pp::panopainter::LegacyStrokeSampleExecutionRequest {
|
||||||
.context = "Canvas::stroke_draw_samples",
|
.context = "Canvas::stroke_draw_samples",
|
||||||
.target_size = { m_width, m_height },
|
.target_size = { m_width, m_height },
|
||||||
.vertices = P,
|
.vertices = P,
|
||||||
|
.sample_points = sample_points,
|
||||||
.copy_stroke_destination = copy_stroke_destination,
|
.copy_stroke_destination = copy_stroke_destination,
|
||||||
.bind_destination_texture = [&] {
|
.bind_destination_texture = [&] {
|
||||||
set_active_texture_unit(1);
|
set_active_texture_unit(1);
|
||||||
@@ -676,6 +709,7 @@ void Canvas::stroke_draw()
|
|||||||
const auto stroke_rasterization = canvas_stroke_rasterization_plan(m_width, m_height);
|
const auto stroke_rasterization = canvas_stroke_rasterization_plan(m_width, m_height);
|
||||||
const bool copy_stroke_destination = stroke_rasterization.copy_stroke_destination;
|
const bool copy_stroke_destination = stroke_rasterization.copy_stroke_destination;
|
||||||
const auto stroke_material = canvas_stroke_material_plan(*brush, copy_stroke_destination);
|
const auto stroke_material = canvas_stroke_material_plan(*brush, copy_stroke_destination);
|
||||||
|
const auto stroke_extent = canvas_stroke_extent(m_width, m_height);
|
||||||
|
|
||||||
apply_canvas_capability(blend_state(), false);
|
apply_canvas_capability(blend_state(), false);
|
||||||
pp::panopainter::setup_legacy_stroke_shader(
|
pp::panopainter::setup_legacy_stroke_shader(
|
||||||
@@ -745,8 +779,16 @@ void Canvas::stroke_draw()
|
|||||||
|
|
||||||
m_tmp[i].unbindFramebuffer();
|
m_tmp[i].unbindFramebuffer();
|
||||||
|
|
||||||
m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width));
|
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
|
||||||
box_face[i] = box_union(box_face[i], box_sample);
|
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
|
||||||
|
.extent = stroke_extent,
|
||||||
|
.previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]),
|
||||||
|
.previous_pass_dirty_box = canvas_stroke_box(box_face[i]),
|
||||||
|
.sample_dirty_box = canvas_stroke_box(box_sample),
|
||||||
|
.include_in_committed_dirty_box = true,
|
||||||
|
});
|
||||||
|
m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box);
|
||||||
|
box_face[i] = glm_box(dirty_update.pass_dirty_box);
|
||||||
// TODO: maybe average color?
|
// TODO: maybe average color?
|
||||||
pad_color = f.col;
|
pad_color = f.col;
|
||||||
}
|
}
|
||||||
@@ -779,34 +821,37 @@ void Canvas::stroke_draw()
|
|||||||
{
|
{
|
||||||
if (!box_dirty[i])
|
if (!box_dirty[i])
|
||||||
continue;
|
continue;
|
||||||
const auto& b = box_face[i];
|
const auto pad_region = pp::paint_renderer::plan_canvas_stroke_pad_region(
|
||||||
glm::vec2 box_size = zw(b) - xy(b);
|
pp::paint_renderer::CanvasStrokePadRegionRequest {
|
||||||
glm::vec2 pad = { 20, 20 }; // pixels padding
|
.extent = stroke_extent,
|
||||||
glm::vec4 pad_box = {
|
.pass_dirty_box = canvas_stroke_box(box_face[i]),
|
||||||
glm::max({0, 0}, xy(b) - pad) * 2.f / m_size - 1.f,
|
});
|
||||||
glm::min(m_size, zw(b) + pad) * 2.f / m_size - 1.f
|
if (!pad_region.has_pixels)
|
||||||
};
|
continue;
|
||||||
// B(xw)--(zw)C box
|
// B(xw)--(zw)C box
|
||||||
// | // | coordinates
|
// | // | coordinates
|
||||||
// A(xy)--(zy)D mapping
|
// A(xy)--(zy)D mapping
|
||||||
std::array<vertex_t, 6> pad_quad = {
|
std::array<vertex_t, 6> pad_quad = {
|
||||||
vertex_t({pad_box.x, pad_box.y}), // A
|
vertex_t({pad_region.ndc_quad[0].x, pad_region.ndc_quad[0].y}), // A
|
||||||
vertex_t({pad_box.x, pad_box.w}), // B
|
vertex_t({pad_region.ndc_quad[1].x, pad_region.ndc_quad[1].y}), // B
|
||||||
vertex_t({pad_box.z, pad_box.w}), // C
|
vertex_t({pad_region.ndc_quad[2].x, pad_region.ndc_quad[2].y}), // C
|
||||||
vertex_t({pad_box.x, pad_box.y}), // A
|
vertex_t({pad_region.ndc_quad[3].x, pad_region.ndc_quad[3].y}), // A
|
||||||
vertex_t({pad_box.z, pad_box.w}), // C
|
vertex_t({pad_region.ndc_quad[4].x, pad_region.ndc_quad[4].y}), // C
|
||||||
vertex_t({pad_box.z, pad_box.y}), // D
|
vertex_t({pad_region.ndc_quad[5].x, pad_region.ndc_quad[5].y}), // D
|
||||||
};
|
};
|
||||||
m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());
|
m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());
|
||||||
|
|
||||||
m_tmp[i].bindFramebuffer();
|
m_tmp[i].bindFramebuffer();
|
||||||
if (copy_stroke_destination)
|
if (copy_stroke_destination)
|
||||||
{
|
{
|
||||||
glm::vec2 o = glm::max({0, 0}, xy(b) - pad);
|
|
||||||
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
|
|
||||||
m_tex[i].bind();
|
m_tex[i].bind();
|
||||||
if (sz.x > 0 && sz.y > 0)
|
copy_framebuffer_to_texture_2d(
|
||||||
copy_framebuffer_to_texture_2d(o.x, o.y, o.x, o.y, sz.x, sz.y);
|
pad_region.copy_region.x,
|
||||||
|
pad_region.copy_region.y,
|
||||||
|
pad_region.copy_region.x,
|
||||||
|
pad_region.copy_region.y,
|
||||||
|
pad_region.copy_region.width,
|
||||||
|
pad_region.copy_region.height);
|
||||||
}
|
}
|
||||||
m_brush_shape.draw_fill();
|
m_brush_shape.draw_fill();
|
||||||
m_tmp[i].unbindFramebuffer();
|
m_tmp[i].unbindFramebuffer();
|
||||||
@@ -845,8 +890,16 @@ void Canvas::stroke_draw()
|
|||||||
m_tmp_dual[i].unbindFramebuffer();
|
m_tmp_dual[i].unbindFramebuffer();
|
||||||
|
|
||||||
// this mode overflows the main brush boundries
|
// this mode overflows the main brush boundries
|
||||||
if (stroke_material.composite_pass.dual_blend_mode == 0)
|
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
|
||||||
m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width));
|
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
|
||||||
|
.extent = stroke_extent,
|
||||||
|
.previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]),
|
||||||
|
.previous_pass_dirty_box = canvas_stroke_box(box_sample),
|
||||||
|
.sample_dirty_box = canvas_stroke_box(box_sample),
|
||||||
|
.include_in_committed_dirty_box =
|
||||||
|
stroke_material.composite_pass.dual_blend_mode == 0,
|
||||||
|
});
|
||||||
|
m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "paint_renderer/compositor.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -13,6 +14,7 @@ struct LegacyStrokeSampleExecutionRequest {
|
|||||||
std::string_view context;
|
std::string_view context;
|
||||||
glm::vec2 target_size {};
|
glm::vec2 target_size {};
|
||||||
std::span<const vertex_t> vertices;
|
std::span<const vertex_t> vertices;
|
||||||
|
std::span<const pp::paint_renderer::CanvasStrokePoint> sample_points;
|
||||||
bool copy_stroke_destination = false;
|
bool copy_stroke_destination = false;
|
||||||
std::function<void()> bind_destination_texture;
|
std::function<void()> bind_destination_texture;
|
||||||
std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_destination_texture;
|
std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_destination_texture;
|
||||||
@@ -49,25 +51,28 @@ struct LegacyStrokeSampleExecutionResult {
|
|||||||
request.bind_destination_texture();
|
request.bind_destination_texture();
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec2 bb_min(request.target_size);
|
const auto sample_bounds = pp::paint_renderer::plan_canvas_stroke_sample_bounds(
|
||||||
glm::vec2 bb_max(0.0f, 0.0f);
|
pp::paint_renderer::CanvasStrokeSampleBoundsRequest {
|
||||||
for (const auto& vertex : request.vertices) {
|
.extent = pp::renderer::Extent2D {
|
||||||
bb_min = glm::max({ 0.0f, 0.0f }, glm::min(bb_min, xy(vertex.pos)));
|
.width = static_cast<std::uint32_t>(request.target_size.x),
|
||||||
bb_max = glm::min(request.target_size, glm::max(bb_max, xy(vertex.pos)));
|
.height = static_cast<std::uint32_t>(request.target_size.y),
|
||||||
|
},
|
||||||
|
.vertices = request.sample_points,
|
||||||
|
});
|
||||||
|
if (!sample_bounds.has_pixels) {
|
||||||
|
if (request.copy_stroke_destination) {
|
||||||
|
request.unbind_destination_texture();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto bb_sz = bb_max - bb_min;
|
result.copy_position = glm::ivec2(sample_bounds.copy_region.x, sample_bounds.copy_region.y);
|
||||||
const glm::vec2 pad(1.0f);
|
result.copy_size = glm::ivec2(sample_bounds.copy_region.width, sample_bounds.copy_region.height);
|
||||||
const glm::ivec2 target_extent(request.target_size);
|
result.dirty_bounds = glm::vec4(
|
||||||
result.copy_position = glm::clamp(
|
sample_bounds.dirty_bounds.min_x,
|
||||||
glm::floor(bb_min) - pad,
|
sample_bounds.dirty_bounds.min_y,
|
||||||
glm::vec2(0.0f),
|
sample_bounds.dirty_bounds.max_x,
|
||||||
request.target_size);
|
sample_bounds.dirty_bounds.max_y);
|
||||||
result.copy_size = glm::clamp(
|
|
||||||
glm::ceil(bb_sz) + pad * 2.0f,
|
|
||||||
glm::vec2(0.0f),
|
|
||||||
glm::vec2(target_extent - result.copy_position));
|
|
||||||
result.dirty_bounds = glm::vec4(result.copy_position, result.copy_position + result.copy_size);
|
|
||||||
|
|
||||||
if (request.copy_stroke_destination) {
|
if (request.copy_stroke_destination) {
|
||||||
request.copy_framebuffer_to_destination_texture(
|
request.copy_framebuffer_to_destination_texture(
|
||||||
|
|||||||
@@ -253,11 +253,18 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(
|
|||||||
bool copy_stroke_destination)
|
bool copy_stroke_destination)
|
||||||
{
|
{
|
||||||
const glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() };
|
const glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() };
|
||||||
|
const std::array<pp::paint_renderer::CanvasStrokePoint, 4> sample_points {
|
||||||
|
pp::paint_renderer::CanvasStrokePoint { .x = P[0].pos.x, .y = P[0].pos.y },
|
||||||
|
pp::paint_renderer::CanvasStrokePoint { .x = P[1].pos.x, .y = P[1].pos.y },
|
||||||
|
pp::paint_renderer::CanvasStrokePoint { .x = P[2].pos.x, .y = P[2].pos.y },
|
||||||
|
pp::paint_renderer::CanvasStrokePoint { .x = P[3].pos.x, .y = P[3].pos.y },
|
||||||
|
};
|
||||||
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
|
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
|
||||||
pp::panopainter::LegacyStrokeSampleExecutionRequest {
|
pp::panopainter::LegacyStrokeSampleExecutionRequest {
|
||||||
.context = "NodeStrokePreview::stroke_draw_samples",
|
.context = "NodeStrokePreview::stroke_draw_samples",
|
||||||
.target_size = size,
|
.target_size = size,
|
||||||
.vertices = P,
|
.vertices = P,
|
||||||
|
.sample_points = sample_points,
|
||||||
.copy_stroke_destination = copy_stroke_destination,
|
.copy_stroke_destination = copy_stroke_destination,
|
||||||
.bind_destination_texture = [&] {
|
.bind_destination_texture = [&] {
|
||||||
set_active_texture_unit(1U);
|
set_active_texture_unit(1U);
|
||||||
|
|||||||
@@ -82,6 +82,34 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] CanvasStrokeBox box_union(CanvasStrokeBox lhs, CanvasStrokeBox rhs) noexcept
|
||||||
|
{
|
||||||
|
return CanvasStrokeBox {
|
||||||
|
.min_x = std::min(lhs.min_x, rhs.min_x),
|
||||||
|
.min_y = std::min(lhs.min_y, rhs.min_y),
|
||||||
|
.max_x = std::max(lhs.max_x, rhs.max_x),
|
||||||
|
.max_y = std::max(lhs.max_y, rhs.max_y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] CanvasStrokeBox clamp_box_to_legacy_dirty_extent(
|
||||||
|
CanvasStrokeBox box,
|
||||||
|
pp::renderer::Extent2D extent) noexcept
|
||||||
|
{
|
||||||
|
const auto max_value = static_cast<float>(extent.width);
|
||||||
|
return CanvasStrokeBox {
|
||||||
|
.min_x = std::clamp(box.min_x, 0.0F, max_value),
|
||||||
|
.min_y = std::clamp(box.min_y, 0.0F, max_value),
|
||||||
|
.max_x = std::clamp(box.max_x, 0.0F, max_value),
|
||||||
|
.max_y = std::clamp(box.max_y, 0.0F, max_value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool has_positive_area(CanvasStrokeBox box) noexcept
|
||||||
|
{
|
||||||
|
return box.max_x > box.min_x && box.max_y > box.min_y;
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept
|
[[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept
|
||||||
{
|
{
|
||||||
const auto extent_status = pp::renderer::validate_extent(extent);
|
const auto extent_status = pp::renderer::validate_extent(extent);
|
||||||
@@ -1371,6 +1399,117 @@ pp::foundation::Result<CanvasStrokeRasterizationPlan> plan_canvas_stroke_rasteri
|
|||||||
return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan);
|
return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
|
||||||
|
CanvasStrokeSampleBoundsRequest request) noexcept
|
||||||
|
{
|
||||||
|
CanvasStrokeSampleBoundsPlan plan;
|
||||||
|
if (request.extent.width == 0U || request.extent.height == 0U || request.vertices.empty()) {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto target_width = static_cast<float>(request.extent.width);
|
||||||
|
const auto target_height = static_cast<float>(request.extent.height);
|
||||||
|
auto min_x = target_width;
|
||||||
|
auto min_y = target_height;
|
||||||
|
auto max_x = 0.0F;
|
||||||
|
auto max_y = 0.0F;
|
||||||
|
for (const auto& vertex : request.vertices) {
|
||||||
|
min_x = std::max(0.0F, std::min(min_x, vertex.x));
|
||||||
|
min_y = std::max(0.0F, std::min(min_y, vertex.y));
|
||||||
|
max_x = std::min(target_width, std::max(max_x, vertex.x));
|
||||||
|
max_y = std::min(target_height, std::max(max_y, vertex.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pad = std::max(0.0F, request.sample_padding_pixels);
|
||||||
|
const auto copy_x = static_cast<int>(std::clamp(std::floor(min_x) - pad, 0.0F, target_width));
|
||||||
|
const auto copy_y = static_cast<int>(std::clamp(std::floor(min_y) - pad, 0.0F, target_height));
|
||||||
|
const auto max_width = static_cast<int>(request.extent.width) - copy_x;
|
||||||
|
const auto max_height = static_cast<int>(request.extent.height) - copy_y;
|
||||||
|
const auto copy_width = static_cast<int>(std::clamp(
|
||||||
|
std::ceil(max_x - min_x) + pad * 2.0F,
|
||||||
|
0.0F,
|
||||||
|
static_cast<float>(max_width)));
|
||||||
|
const auto copy_height = static_cast<int>(std::clamp(
|
||||||
|
std::ceil(max_y - min_y) + pad * 2.0F,
|
||||||
|
0.0F,
|
||||||
|
static_cast<float>(max_height)));
|
||||||
|
|
||||||
|
plan.copy_region = CanvasStrokeCopyRegion {
|
||||||
|
.x = copy_x,
|
||||||
|
.y = copy_y,
|
||||||
|
.width = copy_width,
|
||||||
|
.height = copy_height,
|
||||||
|
};
|
||||||
|
plan.dirty_bounds = CanvasStrokeBox {
|
||||||
|
.min_x = static_cast<float>(copy_x),
|
||||||
|
.min_y = static_cast<float>(copy_y),
|
||||||
|
.max_x = static_cast<float>(copy_x + copy_width),
|
||||||
|
.max_y = static_cast<float>(copy_y + copy_height),
|
||||||
|
};
|
||||||
|
plan.has_pixels = copy_width > 0 && copy_height > 0;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasStrokeFaceDirtyUpdatePlan plan_canvas_stroke_face_dirty_update(
|
||||||
|
CanvasStrokeFaceDirtyUpdateRequest request) noexcept
|
||||||
|
{
|
||||||
|
CanvasStrokeFaceDirtyUpdatePlan plan;
|
||||||
|
plan.accumulated_dirty_box = request.previous_accumulated_dirty_box;
|
||||||
|
plan.pass_dirty_box = box_union(request.previous_pass_dirty_box, request.sample_dirty_box);
|
||||||
|
plan.has_dirty_pixels = has_positive_area(request.sample_dirty_box);
|
||||||
|
plan.pass_dirty = plan.has_dirty_pixels;
|
||||||
|
if (request.include_in_committed_dirty_box && plan.has_dirty_pixels) {
|
||||||
|
plan.accumulated_dirty_box = clamp_box_to_legacy_dirty_extent(
|
||||||
|
box_union(request.previous_accumulated_dirty_box, request.sample_dirty_box),
|
||||||
|
request.extent);
|
||||||
|
plan.committed_dirty = true;
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasStrokePadRegionPlan plan_canvas_stroke_pad_region(
|
||||||
|
CanvasStrokePadRegionRequest request) noexcept
|
||||||
|
{
|
||||||
|
CanvasStrokePadRegionPlan plan;
|
||||||
|
if (request.extent.width == 0U || request.extent.height == 0U) {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pad = std::max(0.0F, request.pad_pixels);
|
||||||
|
const auto width = static_cast<float>(request.extent.width);
|
||||||
|
const auto height = static_cast<float>(request.extent.height);
|
||||||
|
const auto origin_x = std::max(0.0F, request.pass_dirty_box.min_x - pad);
|
||||||
|
const auto origin_y = std::max(0.0F, request.pass_dirty_box.min_y - pad);
|
||||||
|
const auto max_x = std::min(width, request.pass_dirty_box.max_x + pad);
|
||||||
|
const auto max_y = std::min(height, request.pass_dirty_box.max_y + pad);
|
||||||
|
const auto copy_width = max_x - origin_x;
|
||||||
|
const auto copy_height = max_y - origin_y;
|
||||||
|
if (copy_width <= 0.0F || copy_height <= 0.0F) {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto left = origin_x * 2.0F / width - 1.0F;
|
||||||
|
const auto bottom = origin_y * 2.0F / height - 1.0F;
|
||||||
|
const auto right = max_x * 2.0F / width - 1.0F;
|
||||||
|
const auto top = max_y * 2.0F / height - 1.0F;
|
||||||
|
plan.copy_region = CanvasStrokeCopyRegion {
|
||||||
|
.x = static_cast<int>(origin_x),
|
||||||
|
.y = static_cast<int>(origin_y),
|
||||||
|
.width = static_cast<int>(copy_width),
|
||||||
|
.height = static_cast<int>(copy_height),
|
||||||
|
};
|
||||||
|
plan.ndc_quad = {
|
||||||
|
CanvasStrokePoint { .x = left, .y = bottom },
|
||||||
|
CanvasStrokePoint { .x = left, .y = top },
|
||||||
|
CanvasStrokePoint { .x = right, .y = top },
|
||||||
|
CanvasStrokePoint { .x = left, .y = bottom },
|
||||||
|
CanvasStrokePoint { .x = right, .y = top },
|
||||||
|
CanvasStrokePoint { .x = right, .y = bottom },
|
||||||
|
};
|
||||||
|
plan.has_pixels = true;
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
|
const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
|
||||||
{
|
{
|
||||||
switch (path) {
|
switch (path) {
|
||||||
|
|||||||
@@ -149,6 +149,65 @@ struct CanvasStrokeRasterizationPlan {
|
|||||||
bool compatibility_fallback = false;
|
bool compatibility_fallback = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokePoint {
|
||||||
|
float x = 0.0F;
|
||||||
|
float y = 0.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokeBox {
|
||||||
|
float min_x = 0.0F;
|
||||||
|
float min_y = 0.0F;
|
||||||
|
float max_x = 0.0F;
|
||||||
|
float max_y = 0.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokeCopyRegion {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokeSampleBoundsRequest {
|
||||||
|
pp::renderer::Extent2D extent {};
|
||||||
|
std::span<const CanvasStrokePoint> vertices;
|
||||||
|
float sample_padding_pixels = 1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokeSampleBoundsPlan {
|
||||||
|
CanvasStrokeCopyRegion copy_region {};
|
||||||
|
CanvasStrokeBox dirty_bounds {};
|
||||||
|
bool has_pixels = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokeFaceDirtyUpdateRequest {
|
||||||
|
pp::renderer::Extent2D extent {};
|
||||||
|
CanvasStrokeBox previous_accumulated_dirty_box {};
|
||||||
|
CanvasStrokeBox previous_pass_dirty_box {};
|
||||||
|
CanvasStrokeBox sample_dirty_box {};
|
||||||
|
bool include_in_committed_dirty_box = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokeFaceDirtyUpdatePlan {
|
||||||
|
CanvasStrokeBox accumulated_dirty_box {};
|
||||||
|
CanvasStrokeBox pass_dirty_box {};
|
||||||
|
bool has_dirty_pixels = false;
|
||||||
|
bool committed_dirty = false;
|
||||||
|
bool pass_dirty = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokePadRegionRequest {
|
||||||
|
pp::renderer::Extent2D extent {};
|
||||||
|
CanvasStrokeBox pass_dirty_box {};
|
||||||
|
float pad_pixels = 20.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CanvasStrokePadRegionPlan {
|
||||||
|
CanvasStrokeCopyRegion copy_region {};
|
||||||
|
std::array<CanvasStrokePoint, 6> ndc_quad {};
|
||||||
|
bool has_pixels = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct DocumentFaceCompositeRequest {
|
struct DocumentFaceCompositeRequest {
|
||||||
const pp::document::CanvasDocument* document = nullptr;
|
const pp::document::CanvasDocument* document = nullptr;
|
||||||
std::size_t frame_index = 0;
|
std::size_t frame_index = 0;
|
||||||
@@ -393,6 +452,15 @@ export_document_animation_frames_equirectangular_pngs(
|
|||||||
pp::renderer::RenderDeviceFeatures features,
|
pp::renderer::RenderDeviceFeatures features,
|
||||||
pp::renderer::Extent2D extent) noexcept;
|
pp::renderer::Extent2D extent) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
|
||||||
|
CanvasStrokeSampleBoundsRequest request) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] CanvasStrokeFaceDirtyUpdatePlan plan_canvas_stroke_face_dirty_update(
|
||||||
|
CanvasStrokeFaceDirtyUpdateRequest request) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] CanvasStrokePadRegionPlan plan_canvas_stroke_pad_region(
|
||||||
|
CanvasStrokePadRegionRequest request) noexcept;
|
||||||
|
|
||||||
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
|
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ using pp::paint::Rgba;
|
|||||||
using pp::paint::StrokeBlendMode;
|
using pp::paint::StrokeBlendMode;
|
||||||
using pp::assets::decode_png_rgba8;
|
using pp::assets::decode_png_rgba8;
|
||||||
using pp::paint_renderer::CanvasBlendGateRequest;
|
using pp::paint_renderer::CanvasBlendGateRequest;
|
||||||
|
using pp::paint_renderer::CanvasStrokeBox;
|
||||||
|
using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest;
|
||||||
using pp::paint_renderer::CanvasStrokeMaterialRequest;
|
using pp::paint_renderer::CanvasStrokeMaterialRequest;
|
||||||
|
using pp::paint_renderer::CanvasStrokePadRegionRequest;
|
||||||
|
using pp::paint_renderer::CanvasStrokePoint;
|
||||||
|
using pp::paint_renderer::CanvasStrokeSampleBoundsRequest;
|
||||||
using pp::paint_renderer::CanvasStrokeTextureRole;
|
using pp::paint_renderer::CanvasStrokeTextureRole;
|
||||||
using pp::paint_renderer::DocumentFaceCompositeRequest;
|
using pp::paint_renderer::DocumentFaceCompositeRequest;
|
||||||
using pp::paint_renderer::DocumentFrameCompositeRequest;
|
using pp::paint_renderer::DocumentFrameCompositeRequest;
|
||||||
@@ -27,9 +32,12 @@ using pp::paint_renderer::composite_document_face;
|
|||||||
using pp::paint_renderer::composite_document_frame;
|
using pp::paint_renderer::composite_document_frame;
|
||||||
using pp::paint_renderer::export_document_depth_pngs;
|
using pp::paint_renderer::export_document_depth_pngs;
|
||||||
using pp::paint_renderer::plan_canvas_blend_gate;
|
using pp::paint_renderer::plan_canvas_blend_gate;
|
||||||
|
using pp::paint_renderer::plan_canvas_stroke_face_dirty_update;
|
||||||
using pp::paint_renderer::plan_canvas_stroke_feedback;
|
using pp::paint_renderer::plan_canvas_stroke_feedback;
|
||||||
using pp::paint_renderer::plan_canvas_stroke_material;
|
using pp::paint_renderer::plan_canvas_stroke_material;
|
||||||
|
using pp::paint_renderer::plan_canvas_stroke_pad_region;
|
||||||
using pp::paint_renderer::plan_canvas_stroke_rasterization;
|
using pp::paint_renderer::plan_canvas_stroke_rasterization;
|
||||||
|
using pp::paint_renderer::plan_canvas_stroke_sample_bounds;
|
||||||
using pp::paint_renderer::plan_document_depth_export_render;
|
using pp::paint_renderer::plan_document_depth_export_render;
|
||||||
using pp::paint_renderer::plan_stroke_composite;
|
using pp::paint_renderer::plan_stroke_composite;
|
||||||
using pp::paint_renderer::stroke_composite_path_name;
|
using pp::paint_renderer::stroke_composite_path_name;
|
||||||
@@ -1879,6 +1887,169 @@ void plans_canvas_stroke_rasterization_boundary(pp::tests::Harness& h)
|
|||||||
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
|
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void canvas_stroke_sample_bounds_empty_vertices_have_no_pixels(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const auto plan = plan_canvas_stroke_sample_bounds(
|
||||||
|
CanvasStrokeSampleBoundsRequest {
|
||||||
|
.extent = Extent2D { .width = 32, .height = 16 },
|
||||||
|
.vertices = {},
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(h, !plan.has_pixels);
|
||||||
|
PP_EXPECT(h, plan.copy_region.width == 0);
|
||||||
|
PP_EXPECT(h, plan.copy_region.height == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const CanvasStrokePoint vertices[] {
|
||||||
|
{ .x = 10.0F, .y = 20.0F },
|
||||||
|
{ .x = 10.0F, .y = 30.0F },
|
||||||
|
{ .x = 30.0F, .y = 30.0F },
|
||||||
|
{ .x = 30.0F, .y = 20.0F },
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto plan = plan_canvas_stroke_sample_bounds(
|
||||||
|
CanvasStrokeSampleBoundsRequest {
|
||||||
|
.extent = Extent2D { .width = 64, .height = 64 },
|
||||||
|
.vertices = vertices,
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(h, plan.has_pixels);
|
||||||
|
PP_EXPECT(h, plan.copy_region.x == 9);
|
||||||
|
PP_EXPECT(h, plan.copy_region.y == 19);
|
||||||
|
PP_EXPECT(h, plan.copy_region.width == 22);
|
||||||
|
PP_EXPECT(h, plan.copy_region.height == 12);
|
||||||
|
PP_EXPECT(h, near(plan.dirty_bounds.min_x, 9.0F));
|
||||||
|
PP_EXPECT(h, near(plan.dirty_bounds.min_y, 19.0F));
|
||||||
|
PP_EXPECT(h, near(plan.dirty_bounds.max_x, 31.0F));
|
||||||
|
PP_EXPECT(h, near(plan.dirty_bounds.max_y, 31.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
void canvas_stroke_sample_bounds_clamp_out_of_range_vertices(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const CanvasStrokePoint vertices[] {
|
||||||
|
{ .x = -10.0F, .y = -5.0F },
|
||||||
|
{ .x = 70.0F, .y = 80.0F },
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto plan = plan_canvas_stroke_sample_bounds(
|
||||||
|
CanvasStrokeSampleBoundsRequest {
|
||||||
|
.extent = Extent2D { .width = 64, .height = 32 },
|
||||||
|
.vertices = vertices,
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(h, plan.has_pixels);
|
||||||
|
PP_EXPECT(h, plan.copy_region.x == 0);
|
||||||
|
PP_EXPECT(h, plan.copy_region.y == 0);
|
||||||
|
PP_EXPECT(h, plan.copy_region.width == 64);
|
||||||
|
PP_EXPECT(h, plan.copy_region.height == 32);
|
||||||
|
PP_EXPECT(h, near(plan.dirty_bounds.max_x, 64.0F));
|
||||||
|
PP_EXPECT(h, near(plan.dirty_bounds.max_y, 32.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
void canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const auto plan = plan_canvas_stroke_pad_region(
|
||||||
|
CanvasStrokePadRegionRequest {
|
||||||
|
.extent = Extent2D { .width = 100, .height = 80 },
|
||||||
|
.pass_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = 5.0F,
|
||||||
|
.min_y = 10.0F,
|
||||||
|
.max_x = 20.0F,
|
||||||
|
.max_y = 30.0F,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(h, plan.has_pixels);
|
||||||
|
PP_EXPECT(h, plan.copy_region.x == 0);
|
||||||
|
PP_EXPECT(h, plan.copy_region.y == 0);
|
||||||
|
PP_EXPECT(h, plan.copy_region.width == 40);
|
||||||
|
PP_EXPECT(h, plan.copy_region.height == 50);
|
||||||
|
PP_EXPECT(h, near(plan.ndc_quad[0].x, -1.0F));
|
||||||
|
PP_EXPECT(h, near(plan.ndc_quad[0].y, -1.0F));
|
||||||
|
PP_EXPECT(h, near(plan.ndc_quad[2].x, -0.2F));
|
||||||
|
PP_EXPECT(h, near(plan.ndc_quad[2].y, 0.25F));
|
||||||
|
PP_EXPECT(h, near(plan.ndc_quad[5].x, -0.2F));
|
||||||
|
PP_EXPECT(h, near(plan.ndc_quad[5].y, -1.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
void canvas_stroke_face_dirty_update_includes_committed_dirty_box(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const auto plan = plan_canvas_stroke_face_dirty_update(
|
||||||
|
CanvasStrokeFaceDirtyUpdateRequest {
|
||||||
|
.extent = Extent2D { .width = 64, .height = 32 },
|
||||||
|
.previous_accumulated_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = 64.0F,
|
||||||
|
.min_y = 32.0F,
|
||||||
|
.max_x = 0.0F,
|
||||||
|
.max_y = 0.0F,
|
||||||
|
},
|
||||||
|
.previous_pass_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = 64.0F,
|
||||||
|
.min_y = 32.0F,
|
||||||
|
.max_x = 0.0F,
|
||||||
|
.max_y = 0.0F,
|
||||||
|
},
|
||||||
|
.sample_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = -5.0F,
|
||||||
|
.min_y = -3.0F,
|
||||||
|
.max_x = 80.0F,
|
||||||
|
.max_y = 90.0F,
|
||||||
|
},
|
||||||
|
.include_in_committed_dirty_box = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(h, plan.has_dirty_pixels);
|
||||||
|
PP_EXPECT(h, plan.committed_dirty);
|
||||||
|
PP_EXPECT(h, plan.pass_dirty);
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_x, 0.0F));
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_y, 0.0F));
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_x, 64.0F));
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_y, 64.0F));
|
||||||
|
PP_EXPECT(h, near(plan.pass_dirty_box.min_x, -5.0F));
|
||||||
|
PP_EXPECT(h, near(plan.pass_dirty_box.max_y, 90.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
void canvas_stroke_face_dirty_update_can_skip_committed_dirty_box(pp::tests::Harness& h)
|
||||||
|
{
|
||||||
|
const auto plan = plan_canvas_stroke_face_dirty_update(
|
||||||
|
CanvasStrokeFaceDirtyUpdateRequest {
|
||||||
|
.extent = Extent2D { .width = 64, .height = 32 },
|
||||||
|
.previous_accumulated_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = 1.0F,
|
||||||
|
.min_y = 2.0F,
|
||||||
|
.max_x = 3.0F,
|
||||||
|
.max_y = 4.0F,
|
||||||
|
},
|
||||||
|
.previous_pass_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = 10.0F,
|
||||||
|
.min_y = 10.0F,
|
||||||
|
.max_x = 20.0F,
|
||||||
|
.max_y = 20.0F,
|
||||||
|
},
|
||||||
|
.sample_dirty_box = CanvasStrokeBox {
|
||||||
|
.min_x = 0.0F,
|
||||||
|
.min_y = 0.0F,
|
||||||
|
.max_x = 30.0F,
|
||||||
|
.max_y = 30.0F,
|
||||||
|
},
|
||||||
|
.include_in_committed_dirty_box = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(h, plan.has_dirty_pixels);
|
||||||
|
PP_EXPECT(h, !plan.committed_dirty);
|
||||||
|
PP_EXPECT(h, plan.pass_dirty);
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_x, 1.0F));
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_y, 2.0F));
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_x, 3.0F));
|
||||||
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_y, 4.0F));
|
||||||
|
PP_EXPECT(h, near(plan.pass_dirty_box.min_x, 0.0F));
|
||||||
|
PP_EXPECT(h, near(plan.pass_dirty_box.min_y, 0.0F));
|
||||||
|
PP_EXPECT(h, near(plan.pass_dirty_box.max_x, 30.0F));
|
||||||
|
PP_EXPECT(h, near(plan.pass_dirty_box.max_y, 30.0F));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -1920,5 +2091,23 @@ int main()
|
|||||||
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
|
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
|
||||||
harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
|
harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
|
||||||
harness.run("plans_canvas_stroke_rasterization_boundary", plans_canvas_stroke_rasterization_boundary);
|
harness.run("plans_canvas_stroke_rasterization_boundary", plans_canvas_stroke_rasterization_boundary);
|
||||||
|
harness.run(
|
||||||
|
"canvas_stroke_sample_bounds_empty_vertices_have_no_pixels",
|
||||||
|
canvas_stroke_sample_bounds_empty_vertices_have_no_pixels);
|
||||||
|
harness.run(
|
||||||
|
"canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad",
|
||||||
|
canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad);
|
||||||
|
harness.run(
|
||||||
|
"canvas_stroke_sample_bounds_clamp_out_of_range_vertices",
|
||||||
|
canvas_stroke_sample_bounds_clamp_out_of_range_vertices);
|
||||||
|
harness.run(
|
||||||
|
"canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad",
|
||||||
|
canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad);
|
||||||
|
harness.run(
|
||||||
|
"canvas_stroke_face_dirty_update_includes_committed_dirty_box",
|
||||||
|
canvas_stroke_face_dirty_update_includes_committed_dirty_box);
|
||||||
|
harness.run(
|
||||||
|
"canvas_stroke_face_dirty_update_can_skip_committed_dirty_box",
|
||||||
|
canvas_stroke_face_dirty_update_can_skip_committed_dirty_box);
|
||||||
return harness.finish();
|
return harness.finish();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user