星座图优化

This commit is contained in:
2026-08-26 14:38:51 +08:00
parent 6a6a106dda
commit 272d85caac
7 changed files with 118 additions and 59 deletions
+5 -4
View File
@@ -29,9 +29,9 @@ target_include_directories(Aethera_render_2D PUBLIC
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
target_compile_features(Aethera_render_2D PUBLIC cxx_std_20)
target_link_libraries(Aethera_render_2D PUBLIC
Aethera_Kernel
blend2d::blend2d)
target_link_libraries(Aethera_render_2D
PUBLIC Aethera_Kernel
PRIVATE blend2d::blend2d)
if (MSVC)
target_compile_options(Aethera_render_2D PRIVATE /utf-8)
endif ()
@@ -42,7 +42,8 @@ install(TARGETS Aethera_render_2D
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(DIRECTORY "${Aethera_render_2D_source_dir}/"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/render_2D"
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp")
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp"
PATTERN "Blend2D_Cache.ipp" EXCLUDE)
if (RENDERIVE_BUILD_TESTS)
set(Aethera_render_2D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
append_glob_source(Aethera_render_2D_test_sources "${Aethera_render_2D_test_dir}")
+1 -40
View File
@@ -1,29 +1,7 @@
#include "Frame_2D.hpp"
#include <blend2d/core/pixelconverter.h>
#include <algorithm>
#include <cstring>
#include <stdexcept>
namespace aethera::render_2d {
namespace {
const BLPixelConverter& rgba_converter() {
static const BLPixelConverter converter = [] {
BLFormatInfo source{};
if (source.query(BL_FORMAT_PRGB32) != BL_SUCCESS)
throw std::runtime_error("failed to query Blend2D PRGB32 format");
constexpr std::uint8_t sizes[4]{8, 8, 8, 8};
constexpr std::uint8_t shifts[4]{0, 8, 16, 24};
BLFormatInfo destination{};
destination.init(32, BL_FORMAT_FLAG_RGBA, sizes, shifts);
if (destination.sanitize() != BL_SUCCESS)
throw std::runtime_error("failed to describe browser RGBA8 format");
BLPixelConverter result;
if (result.create(destination, source) != BL_SUCCESS)
throw std::runtime_error("failed to create Blend2D RGBA8 converter");
return result;
}();
return converter;
}
}
struct Frame_2D::Private {
Blend2D_Cache color{}; /* Scene 直接合成的原生 PRGB32 颜色层。 */
Pixel_Format output_format{native_pixel_format}; /* 调用方为本帧选择的最终协议格式。 */
@@ -45,24 +23,7 @@ Pixel_Format Frame_2D::output_format() const noexcept {
}
Image_View Frame_2D::image() const { return d->color.view(); }
Pixel_Buffer Frame_2D::output_pixels() const {
const Image_View source = image();
Pixel_Buffer result{{}, source.width, source.height, d->output_format};
if (source.empty()) return result;
const std::size_t row_size = static_cast<std::size_t>(source.width) * 4U;
result.bytes.resize(row_size * static_cast<std::size_t>(source.height));
if (d->output_format == native_pixel_format) {
for (int y = 0; y < source.height; ++y)
std::memcpy(result.bytes.data() + static_cast<std::size_t>(y) * row_size,
source.data + static_cast<std::ptrdiff_t>(y) * source.stride,
row_size);
return result;
}
if (rgba_converter().convert_rect(
result.bytes.data(), static_cast<intptr_t>(row_size), source.data,
source.stride, static_cast<std::uint32_t>(source.width),
static_cast<std::uint32_t>(source.height)) != BL_SUCCESS)
throw std::runtime_error("Blend2D failed to convert PRGB32 to RGBA8");
return result;
return d->color.output_pixels(d->output_format);
}
Blend2D_Cache& detail::Frame_2D_Access::render_target(Frame_2D* frame) {
if (!frame)
+1 -1
View File
@@ -21,7 +21,7 @@ public:
explicit Frame_2D(Frame_Identity identity,
Pixel_Format output_format = native_pixel_format);
~Frame_2D() override;
/* 保留 Blend2D 图像分配,只重启物理槽承载的新逻辑帧。 */
/* 保留二维后端图像分配,只重启物理槽承载的新逻辑帧。 */
void begin(Frame_Identity identity,
Pixel_Format output_format = native_pixel_format);
[[nodiscard]] Pixel_Format output_format() const noexcept;
@@ -101,8 +101,13 @@ void Constellation_Diagram::Private::paint(Object* object) {
return;
}
detail::Painter painter(cache, prepared.canvas);
for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid});
for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid});
/*
* 原实现每个点都会创建 BLPath,再分别 fill + stroke;这里改为同色实心圆批量入口。
* 旧样式是 radius + 1px 同色描边,最终外半径约增加 0.5px,因此直接使用 4.5/2.5
* 保持视觉尺寸,同时只做一次填充状态设置并走后端原生 circle primitive。
*/
painter.solid_circles(prepared.anchors, 4.5, state.anchor_color);
painter.solid_circles(prepared.points, 2.5, state.point_color);
}
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
+77 -7
View File
@@ -1,4 +1,5 @@
#include "Blend2D_Cache.hpp"
#include "Blend2D_Cache.ipp"
#include <algorithm>
#include <cmath>
#include <cstring>
@@ -22,6 +23,24 @@ void Blend2D_Cache::Private::clear_image(BLImage& image) {
std::memset(row, 0, static_cast<std::size_t>(data.size.w) * sizeof(Pixel));
}
}
const BLPixelConverter& Blend2D_Cache::Private::rgba_converter() {
static const BLPixelConverter converter = [] {
BLFormatInfo source{};
if (source.query(BL_FORMAT_PRGB32) != BL_SUCCESS)
throw std::runtime_error("failed to query Blend2D PRGB32 format");
constexpr std::uint8_t sizes[4]{8, 8, 8, 8};
constexpr std::uint8_t shifts[4]{0, 8, 16, 24};
BLFormatInfo destination{};
destination.init(32, BL_FORMAT_FLAG_RGBA, sizes, shifts);
if (destination.sanitize() != BL_SUCCESS)
throw std::runtime_error("failed to describe browser RGBA8 format");
BLPixelConverter result;
if (result.create(destination, source) != BL_SUCCESS)
throw std::runtime_error("failed to create Blend2D RGBA8 converter");
return result;
}();
return converter;
}
Blend2D_Cache::Private::Private(const Private& other) {
require_success(image.assign_deep(other.image), "copy Blend2D image");
}
@@ -81,6 +100,28 @@ Image_View Blend2D_Cache::view() const {
return {static_cast<const std::byte*>(data.pixel_data), data.size.w, data.size.h,
static_cast<int>(data.stride), Pixel_Format::bgra8_premultiplied};
}
Pixel_Buffer Blend2D_Cache::output_pixels(Pixel_Format format) const {
const Image_View source = view();
Pixel_Buffer result{{}, source.width, source.height, format};
if (source.empty()) return result;
const std::size_t row_size = static_cast<std::size_t>(source.width) * sizeof(Pixel);
result.bytes.resize(row_size * static_cast<std::size_t>(source.height));
if (format == Pixel_Format::bgra8_premultiplied) {
for (int y = 0; y < source.height; ++y)
std::memcpy(result.bytes.data() + static_cast<std::size_t>(y) * row_size,
source.data + static_cast<std::ptrdiff_t>(y) * source.stride,
row_size);
return result;
}
if (format != Pixel_Format::rgba8)
throw std::invalid_argument("unsupported 2D output pixel format");
if (Private::rgba_converter().convert_rect(
result.bytes.data(), static_cast<intptr_t>(row_size), source.data,
source.stride, static_cast<std::uint32_t>(source.width),
static_cast<std::uint32_t>(source.height)) != BL_SUCCESS)
throw std::runtime_error("Blend2D failed to convert PRGB32 to RGBA8");
return result;
}
namespace detail {
void Painter::Private::load_font_face(BLFontFace& face, std::initializer_list<const char*> candidates) {
for (const char* path : candidates) {
@@ -120,7 +161,13 @@ BLStrokeJoin Painter::Private::stroke_join(Line_Join join) noexcept {
default: return BL_STROKE_JOIN_MITER_CLIP;
}
}
void Painter::Private::apply_fill(Color color) {
if (fill_color && *fill_color == color) return;
context.set_fill_style(rgba(color));
fill_color = color;
}
void Painter::Private::apply_pen(const Pen& pen) {
if (stroke_pen && *stroke_pen == pen) return;
context.set_stroke_style(rgba(pen.color));
context.set_stroke_width(std::max(0.1, pen.width));
context.set_stroke_caps(stroke_cap(pen.cap));
@@ -134,6 +181,7 @@ void Painter::Private::apply_pen(const Pen& pen) {
pattern.append(std::max(1.0, pen.width * 2.0));
}
context.set_stroke_dash_array(pattern);
stroke_pen = pen;
}
BLFont Painter::Private::make_font(const Font& font) {
BLFont result;
@@ -204,7 +252,11 @@ Painter::Clip_Scope::Clip_Scope(Painter& owner, Rect_F rect) {
painter = &owner;
}
Painter::Clip_Scope::~Clip_Scope() {
if (painter) painter->d->context.restore();
if (!painter) return;
painter->d->context.restore();
/* restore 会回滚绘制状态;同步失效本地样式缓存,避免后续错误跳过状态设置。 */
painter->d->fill_color.reset();
painter->d->stroke_pen.reset();
}
Painter::Clip_Scope::Clip_Scope(Clip_Scope&& other) noexcept : painter(std::exchange(other.painter, nullptr)) {}
Painter::operator bool() const noexcept { return d->active; }
@@ -231,26 +283,44 @@ void Painter::polygon(std::span<const Point_F> points, const Pen& pen, const Bru
BLPath path; path.move_to(points.front().x, points.front().y);
for (std::size_t index = 1; index < points.size(); ++index) path.line_to(points[index].x, points[index].y);
path.close();
if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_path(path); }
if (brush.enabled()) { d->apply_fill(brush.color); d->context.fill_path(path); }
if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_path(path); }
}
void Painter::rect(Rect_F value, const Pen& pen, const Brush& brush) {
value = value.normalized(); if (!d->active || value.empty()) return;
const BLRect shape(value.x, value.y, value.width, value.height);
if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_rect(shape); }
if (brush.enabled()) { d->apply_fill(brush.color); d->context.fill_rect(shape); }
if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_rect(shape); }
}
void Painter::circle(Point_F center, double radius, const Pen& pen, const Brush& brush) {
if (!d->active || radius <= 0.0) return;
BLPath path; path.add_ellipse(BLEllipse(center.x, center.y, radius, radius));
if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_path(path); }
if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_path(path); }
/* 同色实心填充 + 实线描边等价为扩大半径的一次实心圆,避免重复栅格化。 */
if (brush.enabled() && pen.enabled() && brush.style == Brush_Style::solid &&
pen.style == Line_Style::solid && brush.color == pen.color) {
d->apply_fill(brush.color);
d->context.fill_circle(center.x, center.y, radius + pen.width * 0.5);
return;
}
if (brush.enabled()) {
d->apply_fill(brush.color);
d->context.fill_circle(center.x, center.y, radius);
}
if (pen.enabled()) {
d->apply_pen(pen);
d->context.stroke_circle(center.x, center.y, radius);
}
}
void Painter::solid_circles(std::span<const Point_F> centers, double radius, Color color) {
if (!d->active || centers.empty() || radius <= 0.0 || color.alpha == 0) return;
d->apply_fill(color);
for (const auto& center : centers)
d->context.fill_circle(center.x, center.y, radius);
}
void Painter::text(Point_F position, std::string_view value, const Font& font, const Pen& pen,
double rotation_degrees) {
if (!d->active || value.empty() || !pen.enabled()) return;
BLFont bl_font = Private::make_font(font); if (!bl_font) return;
d->context.set_fill_style(Private::rgba(pen.color));
d->apply_fill(pen.color);
const double baseline = position.y + bl_font.metrics().ascent;
const bool rotated = std::isfinite(rotation_degrees) && rotation_degrees != 0.0;
if (rotated) { d->context.save(); d->context.rotate(rotation_degrees * std::numbers::pi / 180.0, position.x, baseline); }
+18 -4
View File
@@ -8,7 +8,12 @@ namespace aethera::render_2d {
namespace detail {
struct Painter;
}
/* 可作为双缓冲值深拷贝的 Blend2D 像素缓存。 */
/*
* 二维像素缓存的后端封装。
*
* 该头文件故意不包含任何 Blend2D 头文件,也不暴露 BL* 类型;
* 具体 Blend2D 图像/上下文实现全部留在 render 目录的实现文件中。
*/
struct Blend2D_Cache : Color_Cache {
struct Private;
Blend2D_Cache();
@@ -27,12 +32,17 @@ struct Blend2D_Cache : Color_Cache {
[[nodiscard]] Size size() const noexcept;
/* 返回由缓存拥有、在下次非 const 修改前有效的只读像素视图。 */
[[nodiscard]] Image_View view() const;
/*
* 将内部原生像素复制/转换为指定协议格式。
* 像素转换也由 render 后端完成,调用方无需包含任何 Blend2D 头文件。
*/
[[nodiscard]] Pixel_Buffer output_pixels(Pixel_Format format) const;
private:
friend struct detail::Painter;
std::unique_ptr<Private> d; /* Blend2D 图像实现的唯一所有权。 */
};
namespace detail {
/* 只在 Renderable::Private 绘制实现中使用的 Blend2D 画笔。 */
/* 只在 Renderable::Private 绘制实现中使用的二维画笔;后端类型保持隐藏。 */
struct Painter {
struct Private;
struct Clip_Scope {
@@ -58,6 +68,11 @@ struct Painter {
void polygon(std::span<const Point_F> points, const Pen& pen, const Brush& brush);
void rect(Rect_F rect, const Pen& pen, const Brush& brush = {});
void circle(Point_F center, double radius, const Pen& pen, const Brush& brush = {});
/*
* 同色实心圆批量入口。内部只设置一次填充状态,并走后端原生 circle primitive
* 用于星座图等大量小圆点,避免每个点创建通用 Path 和重复设置样式。
*/
void solid_circles(std::span<const Point_F> centers, double radius, Color color);
void text(Point_F position,
std::string_view value,
const Font& font,
@@ -69,8 +84,7 @@ struct Painter {
std::span<const Pixel> pixels,
Image_Interpolation_Mode interpolation);
private:
std::unique_ptr<Private> d; /* 当前 BLContext 及绘制辅助算法的唯一所有权。 */
std::unique_ptr<Private> d; /* 当前后端绘制上下文及辅助算法的唯一所有权。 */
};
}
}
#include "Blend2D_Cache.ipp"
+9 -1
View File
@@ -1,7 +1,9 @@
#pragma once
#include <blend2d/blend2d.h>
#include <blend2d/core/pixelconverter.h>
#include <array>
#include <initializer_list>
#include <optional>
#include <vector>
namespace aethera::render_2d {
struct Blend2D_Cache::Private {
@@ -13,14 +15,20 @@ struct Blend2D_Cache::Private {
static void require_success(BLResult result, const char* operation);
/* 将图像全部清零为透明像素。 */
static void clear_image(BLImage& image);
/* 进程级 PRGB32 -> RGBA8 转换器,只在后端实现中可见。 */
[[nodiscard]] static const BLPixelConverter& rgba_converter();
};
namespace detail {
struct Painter::Private {
BLContext context{}; /* 当前缓存图像上的 Blend2D 绘制上下文。 */
bool active{}; /* context 是否已经成功 begin 且尚未 end。 */
std::optional<Color> fill_color{}; /* 已应用的填充颜色,减少重复状态写入。 */
std::optional<Pen> stroke_pen{}; /* 已应用的描边参数,减少重复状态写入。 */
/* 将项目颜色转换为 Blend2D 浮点颜色。 */
[[nodiscard]] static BLRgba rgba(Color color) noexcept;
/* 按 Pen 配置当前描边样式。 */
/* 仅当颜色变化时更新当前填充样式。 */
void apply_fill(Color color);
/* 仅当 Pen 变化时更新当前描边样式。 */
void apply_pen(const Pen& pen);
/* 从系统字体候选中创建对应字号和字形的 Blend2D 字体。 */
[[nodiscard]] static BLFont make_font(const Font& font);