From c1fa90b1014657824e49158d4f526f8c967dbfce Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 9 Feb 2026 07:40:36 +0900 Subject: [PATCH] =?UTF-8?q?DistributorNode=E3=83=BBNinePatchSourceNode?= =?UTF-8?q?=E3=81=AE=E3=83=A6=E3=83=8B=E3=83=83=E3=83=88=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DistributorNode: 構築、ポート管理、単一/複数出力パイプライン、アフィン伝播 - NinePatchSourceNode: 構築、setupWithBounds、setupFromNinePatch解析、 出力サイズ変更、クリッピング、位置オフセット、無効ソース処理 - 計23テストケース追加(合計202テストケース) --- test/distributor_node_test.cpp | 312 +++++++++++++++++++++ test/ninepatch_source_node_test.cpp | 403 ++++++++++++++++++++++++++++ 2 files changed, 715 insertions(+) create mode 100644 test/distributor_node_test.cpp create mode 100644 test/ninepatch_source_node_test.cpp diff --git a/test/distributor_node_test.cpp b/test/distributor_node_test.cpp new file mode 100644 index 0000000..0d1e0bd --- /dev/null +++ b/test/distributor_node_test.cpp @@ -0,0 +1,312 @@ +// fleximg DistributorNode Unit Tests +// 分配ノードのテスト + +#include "doctest.h" + +#define FLEXIMG_NAMESPACE fleximg +#include "fleximg/core/common.h" +#include "fleximg/core/types.h" +#include "fleximg/image/image_buffer.h" +#include "fleximg/image/render_types.h" +#include "fleximg/nodes/distributor_node.h" +#include "fleximg/nodes/renderer_node.h" +#include "fleximg/nodes/sink_node.h" +#include "fleximg/nodes/source_node.h" + +#include + +using namespace fleximg; + +// ============================================================================= +// Helper Functions +// ============================================================================= + +static ImageBuffer createSolidImage(int width, int height, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + ImageBuffer img(width, height, PixelFormatIDs::RGBA8_Straight); + ViewPort view = img.view(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + uint8_t *p = static_cast(view.pixelAt(x, y)); + p[0] = r; + p[1] = g; + p[2] = b; + p[3] = a; + } + } + return img; +} + +static void getPixelRGBA8(const ViewPort &view, int x, int y, uint8_t &r, + uint8_t &g, uint8_t &b, uint8_t &a) { + const uint8_t *p = static_cast(view.pixelAt(x, y)); + r = p[0]; + g = p[1]; + b = p[2]; + a = p[3]; +} + +static bool hasNonZeroPixels(const ViewPort &view) { + for (int y = 0; y < view.height; y++) { + const uint8_t *row = static_cast(view.pixelAt(0, y)); + for (int x = 0; x < view.width; x++) { + if (row[x * 4 + 3] > 0) + return true; + } + } + return false; +} + +// ============================================================================= +// DistributorNode Construction Tests +// ============================================================================= + +TEST_CASE("DistributorNode basic construction") { + SUBCASE("default 1 output") { + DistributorNode node; + CHECK(node.outputCount() == 1); + CHECK(node.name() != nullptr); + } + + SUBCASE("custom output count") { + DistributorNode node2(2); + CHECK(node2.outputCount() == 2); + + DistributorNode node5(5); + CHECK(node5.outputCount() == 5); + } +} + +TEST_CASE("DistributorNode setOutputCount") { + DistributorNode node; + CHECK(node.outputCount() == 1); + + node.setOutputCount(4); + CHECK(node.outputCount() == 4); + + node.setOutputCount(1); + CHECK(node.outputCount() == 1); + + // 0以下は1にクランプ + node.setOutputCount(0); + CHECK(node.outputCount() == 1); + + node.setOutputCount(-1); + CHECK(node.outputCount() == 1); +} + +// ============================================================================= +// DistributorNode Port Tests +// ============================================================================= + +TEST_CASE("DistributorNode port access") { + DistributorNode node(3); + + SUBCASE("input port exists") { CHECK(node.inputPort(0) != nullptr); } + + SUBCASE("output ports exist") { + CHECK(node.outputPort(0) != nullptr); + CHECK(node.outputPort(1) != nullptr); + CHECK(node.outputPort(2) != nullptr); + } +} + +// ============================================================================= +// DistributorNode Pipeline Tests +// ============================================================================= + +TEST_CASE("DistributorNode single output pipeline") { + const int imgSize = 32; + const int canvasSize = 64; + + ImageBuffer srcImg = createSolidImage(imgSize, imgSize, 255, 0, 0, 255); + ImageBuffer dstImg(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + SourceNode src(srcImg.view(), float_to_fixed(imgSize / 2.0f), + float_to_fixed(imgSize / 2.0f)); + RendererNode renderer; + DistributorNode distributor(1); + SinkNode sink(dstImg.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + + src >> renderer >> distributor; + distributor.connectTo(sink, 0, 0); + + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // 出力に赤いピクセルがあることを確認 + bool foundRed = false; + for (int y = 0; y < canvasSize && !foundRed; y++) { + for (int x = 0; x < canvasSize && !foundRed; x++) { + uint8_t r, g, b, a; + getPixelRGBA8(dstImg.view(), x, y, r, g, b, a); + if (r > 128 && a > 128) + foundRed = true; + } + } + CHECK(foundRed); +} + +TEST_CASE("DistributorNode two outputs receive same image") { + const int imgSize = 32; + const int canvasSize = 64; + + ImageBuffer srcImg = createSolidImage(imgSize, imgSize, 255, 0, 0, 255); + ImageBuffer dstImg1(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + ImageBuffer dstImg2(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + SourceNode src(srcImg.view(), float_to_fixed(imgSize / 2.0f), + float_to_fixed(imgSize / 2.0f)); + RendererNode renderer; + DistributorNode distributor(2); + SinkNode sink1(dstImg1.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + SinkNode sink2(dstImg2.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + + src >> renderer >> distributor; + distributor.connectTo(sink1, 0, 0); + distributor.connectTo(sink2, 0, 1); + + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // 両方の出力に赤いピクセルがあることを確認 + CHECK(hasNonZeroPixels(dstImg1.view())); + CHECK(hasNonZeroPixels(dstImg2.view())); + + // 両方の出力が同じ画像であることを確認(フォーマット変換の微差を許容) + bool match = true; + for (int y = 0; y < canvasSize && match; y++) { + for (int x = 0; x < canvasSize && match; x++) { + uint8_t r1, g1, b1, a1, r2, g2, b2, a2; + getPixelRGBA8(dstImg1.view(), x, y, r1, g1, b1, a1); + getPixelRGBA8(dstImg2.view(), x, y, r2, g2, b2, a2); + int dr = std::abs(static_cast(r1) - static_cast(r2)); + int dg = std::abs(static_cast(g1) - static_cast(g2)); + int db = std::abs(static_cast(b1) - static_cast(b2)); + int da = std::abs(static_cast(a1) - static_cast(a2)); + if (dr > 2 || dg > 2 || db > 2 || da > 2) + match = false; + } + } + CHECK(match); +} + +TEST_CASE("DistributorNode no connected outputs") { + const int imgSize = 32; + const int canvasSize = 64; + + ImageBuffer srcImg = createSolidImage(imgSize, imgSize, 255, 0, 0, 255); + + SourceNode src(srcImg.view(), float_to_fixed(imgSize / 2.0f), + float_to_fixed(imgSize / 2.0f)); + RendererNode renderer; + DistributorNode distributor(2); // 2出力だが接続なし + + src >> renderer >> distributor; + + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // エラーなく完了すればOK + CHECK(true); +} + +// ============================================================================= +// DistributorNode Affine Transform Tests +// ============================================================================= + +TEST_CASE("DistributorNode affine transform propagation") { + const int imgSize = 32; + const int canvasSize = 64; + + ImageBuffer srcImg = createSolidImage(imgSize, imgSize, 0, 255, 0, 255); + ImageBuffer dstWithAffine(canvasSize, canvasSize, + PixelFormatIDs::RGBA8_Straight); + ImageBuffer dstWithoutAffine(canvasSize, canvasSize, + PixelFormatIDs::RGBA8_Straight); + + // アフィン変換あり(平行移動) + { + SourceNode src(srcImg.view(), float_to_fixed(imgSize / 2.0f), + float_to_fixed(imgSize / 2.0f)); + RendererNode renderer; + DistributorNode distributor(1); + distributor.setTranslation(10.0f, 10.0f); + SinkNode sink(dstWithAffine.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + + src >> renderer >> distributor; + distributor.connectTo(sink, 0, 0); + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + } + + // アフィン変換なし + { + SourceNode src(srcImg.view(), float_to_fixed(imgSize / 2.0f), + float_to_fixed(imgSize / 2.0f)); + RendererNode renderer; + DistributorNode distributor(1); + SinkNode sink(dstWithoutAffine.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + + src >> renderer >> distributor; + distributor.connectTo(sink, 0, 0); + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + } + + // 両方とも描画されていること + CHECK(hasNonZeroPixels(dstWithAffine.view())); + CHECK(hasNonZeroPixels(dstWithoutAffine.view())); + + // 異なる結果であること(平行移動が適用されている) + bool identical = true; + for (int y = 0; y < canvasSize && identical; y++) { + for (int x = 0; x < canvasSize && identical; x++) { + uint8_t r1, g1, b1, a1, r2, g2, b2, a2; + getPixelRGBA8(dstWithAffine.view(), x, y, r1, g1, b1, a1); + getPixelRGBA8(dstWithoutAffine.view(), x, y, r2, g2, b2, a2); + if (r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2) + identical = false; + } + } + CHECK_FALSE(identical); +} + +TEST_CASE("DistributorNode three outputs") { + const int imgSize = 32; + const int canvasSize = 64; + + ImageBuffer srcImg = createSolidImage(imgSize, imgSize, 0, 0, 255, 255); + ImageBuffer dstImg1(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + ImageBuffer dstImg2(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + ImageBuffer dstImg3(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + SourceNode src(srcImg.view(), float_to_fixed(imgSize / 2.0f), + float_to_fixed(imgSize / 2.0f)); + RendererNode renderer; + DistributorNode distributor(3); + SinkNode sink1(dstImg1.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + SinkNode sink2(dstImg2.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + SinkNode sink3(dstImg3.view(), float_to_fixed(canvasSize / 2.0f), + float_to_fixed(canvasSize / 2.0f)); + + src >> renderer >> distributor; + distributor.connectTo(sink1, 0, 0); + distributor.connectTo(sink2, 0, 1); + distributor.connectTo(sink3, 0, 2); + + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // 3つの出力全てに描画されていること + CHECK(hasNonZeroPixels(dstImg1.view())); + CHECK(hasNonZeroPixels(dstImg2.view())); + CHECK(hasNonZeroPixels(dstImg3.view())); +} diff --git a/test/ninepatch_source_node_test.cpp b/test/ninepatch_source_node_test.cpp new file mode 100644 index 0000000..b4bb229 --- /dev/null +++ b/test/ninepatch_source_node_test.cpp @@ -0,0 +1,403 @@ +// fleximg NinePatchSourceNode Unit Tests +// 9patch画像ソースノードのテスト + +#include "doctest.h" + +#define FLEXIMG_NAMESPACE fleximg +#include "fleximg/core/common.h" +#include "fleximg/core/types.h" +#include "fleximg/image/image_buffer.h" +#include "fleximg/image/render_types.h" +#include "fleximg/nodes/ninepatch_source_node.h" +#include "fleximg/nodes/renderer_node.h" +#include "fleximg/nodes/sink_node.h" + +using namespace fleximg; + +// ============================================================================= +// Helper Functions +// ============================================================================= + +static ImageBuffer createSolidImage(int width, int height, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + ImageBuffer img(width, height, PixelFormatIDs::RGBA8_Straight); + ViewPort view = img.view(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + uint8_t *p = static_cast(view.pixelAt(x, y)); + p[0] = r; + p[1] = g; + p[2] = b; + p[3] = a; + } + } + return img; +} + +// 9patch互換画像を作成(外周1pxがメタデータ) +// 内部画像は単色、上辺と左辺に黒ピクセルで伸縮領域を指定 +static ImageBuffer createNinePatchImage(int innerWidth, int innerHeight, + int stretchXStart, int stretchXEnd, + int stretchYStart, int stretchYEnd, + uint8_t r, uint8_t g, uint8_t b) { + int totalW = innerWidth + 2; + int totalH = innerHeight + 2; + ImageBuffer img(totalW, totalH, PixelFormatIDs::RGBA8_Straight); + ViewPort view = img.view(); + + // 全体を透明で初期化 + for (int y = 0; y < totalH; y++) { + for (int x = 0; x < totalW; x++) { + uint8_t *p = static_cast(view.pixelAt(x, y)); + p[0] = 0; + p[1] = 0; + p[2] = 0; + p[3] = 0; + } + } + + // 内部画像を塗る(不透明) + for (int y = 1; y <= innerHeight; y++) { + for (int x = 1; x <= innerWidth; x++) { + uint8_t *p = static_cast(view.pixelAt(x, y)); + p[0] = r; + p[1] = g; + p[2] = b; + p[3] = 255; + } + } + + // 上辺メタデータ(y=0): 横方向伸縮領域を黒ピクセルで指定 + for (int x = stretchXStart; x <= stretchXEnd; x++) { + uint8_t *p = static_cast(view.pixelAt(x + 1, 0)); + p[0] = 0; + p[1] = 0; + p[2] = 0; + p[3] = 255; // 黒不透明 = 伸縮マーカー + } + + // 左辺メタデータ(x=0): 縦方向伸縮領域を黒ピクセルで指定 + for (int y = stretchYStart; y <= stretchYEnd; y++) { + uint8_t *p = static_cast(view.pixelAt(0, y + 1)); + p[0] = 0; + p[1] = 0; + p[2] = 0; + p[3] = 255; // 黒不透明 = 伸縮マーカー + } + + return img; +} + +static void getPixelRGBA8(const ViewPort &view, int x, int y, uint8_t &r, + uint8_t &g, uint8_t &b, uint8_t &a) { + const uint8_t *p = static_cast(view.pixelAt(x, y)); + r = p[0]; + g = p[1]; + b = p[2]; + a = p[3]; +} + +static bool hasNonZeroPixels(const ViewPort &view) { + for (int y = 0; y < view.height; y++) { + const uint8_t *row = static_cast(view.pixelAt(0, y)); + for (int x = 0; x < view.width; x++) { + if (row[x * 4 + 3] > 0) + return true; + } + } + return false; +} + +// ============================================================================= +// NinePatchSourceNode Construction Tests +// ============================================================================= + +TEST_CASE("NinePatchSourceNode basic construction") { + NinePatchSourceNode node; + CHECK(node.name() != nullptr); + CHECK(node.outputWidth() == doctest::Approx(0.0f)); + CHECK(node.outputHeight() == doctest::Approx(0.0f)); +} + +// ============================================================================= +// NinePatchSourceNode setupWithBounds Tests +// ============================================================================= + +TEST_CASE("NinePatchSourceNode setupWithBounds") { + NinePatchSourceNode node; + + // 20x20 の画像、左右上下各5pxの固定部 + ImageBuffer srcImg = createSolidImage(20, 20, 128, 128, 128, 255); + + node.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + + CHECK(node.srcLeft() == 5); + CHECK(node.srcTop() == 5); + CHECK(node.srcRight() == 5); + CHECK(node.srcBottom() == 5); +} + +TEST_CASE("NinePatchSourceNode setupWithBounds asymmetric") { + NinePatchSourceNode node; + + ImageBuffer srcImg = createSolidImage(30, 20, 128, 128, 128, 255); + + node.setupWithBounds(srcImg.view(), 3, 4, 7, 6); + + CHECK(node.srcLeft() == 3); + CHECK(node.srcTop() == 4); + CHECK(node.srcRight() == 7); + CHECK(node.srcBottom() == 6); +} + +// ============================================================================= +// NinePatchSourceNode setupFromNinePatch Tests +// ============================================================================= + +TEST_CASE("NinePatchSourceNode setupFromNinePatch") { + // 10x10 の内部画像、伸縮領域は [3,6] × [3,6] + // → left=3, right=10-1-6=3, top=3, bottom=10-1-6=3 + ImageBuffer ninePatch = + createNinePatchImage(10, 10, 3, 6, 3, 6, 200, 200, 200); + + NinePatchSourceNode node; + node.setupFromNinePatch(ninePatch.view()); + + CHECK(node.srcLeft() == 3); + CHECK(node.srcTop() == 3); + CHECK(node.srcRight() == 3); + CHECK(node.srcBottom() == 3); +} + +TEST_CASE("NinePatchSourceNode setupFromNinePatch asymmetric") { + // 12x10 の内部画像、伸縮領域は横[2,9]、縦[3,7] + // → left=2, right=12-1-9=2, top=3, bottom=10-1-7=2 + ImageBuffer ninePatch = + createNinePatchImage(12, 10, 2, 9, 3, 7, 200, 200, 200); + + NinePatchSourceNode node; + node.setupFromNinePatch(ninePatch.view()); + + CHECK(node.srcLeft() == 2); + CHECK(node.srcRight() == 2); + CHECK(node.srcTop() == 3); + CHECK(node.srcBottom() == 2); +} + +TEST_CASE("NinePatchSourceNode setupFromNinePatch invalid image") { + SUBCASE("too small") { + // 2x2 は最小サイズ(3x3)未満 + ImageBuffer tinyImg(2, 2, PixelFormatIDs::RGBA8_Straight); + NinePatchSourceNode node; + node.setupFromNinePatch(tinyImg.view()); + // sourceValid_がfalseになるため、パイプラインは空結果を返す + // (直接アクセスする方法がないため、パイプライン実行で確認) + } +} + +// ============================================================================= +// NinePatchSourceNode Output Size Tests +// ============================================================================= + +TEST_CASE("NinePatchSourceNode setOutputSize") { + NinePatchSourceNode node; + node.setOutputSize(100.0f, 50.0f); + + CHECK(node.outputWidth() == doctest::Approx(100.0f)); + CHECK(node.outputHeight() == doctest::Approx(50.0f)); +} + +TEST_CASE("NinePatchSourceNode setPivot") { + NinePatchSourceNode node; + int_fixed px = float_to_fixed(10.0f); + int_fixed py = float_to_fixed(20.0f); + node.setPivot(px, py); + + CHECK(node.pivotX() == px); + CHECK(node.pivotY() == py); +} + +// ============================================================================= +// NinePatchSourceNode Pipeline Tests +// ============================================================================= + +TEST_CASE("NinePatchSourceNode basic pipeline") { + // 内部画像: 20x20, 固定部: 各5px + ImageBuffer srcImg = createSolidImage(20, 20, 255, 0, 0, 255); + + const int canvasSize = 40; + ImageBuffer dstImg(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + NinePatchSourceNode ninePatch; + ninePatch.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + ninePatch.setOutputSize(40.0f, 40.0f); // 20→40に拡大 + + RendererNode renderer; + SinkNode sink(dstImg.view(), 0, 0); + + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // 出力に描画されていること + CHECK(hasNonZeroPixels(dstImg.view())); +} + +TEST_CASE("NinePatchSourceNode same size output") { + // 出力サイズ = ソースサイズ → スケール1.0 + ImageBuffer srcImg = createSolidImage(20, 20, 0, 255, 0, 255); + + const int canvasSize = 20; + ImageBuffer dstImg(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + NinePatchSourceNode ninePatch; + ninePatch.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + ninePatch.setOutputSize(20.0f, 20.0f); + + RendererNode renderer; + SinkNode sink(dstImg.view(), 0, 0); + + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + CHECK(hasNonZeroPixels(dstImg.view())); +} + +TEST_CASE("NinePatchSourceNode stretch only horizontally") { + // 横のみ拡大 + ImageBuffer srcImg = createSolidImage(20, 20, 0, 0, 255, 255); + + const int canvasW = 60; + const int canvasH = 20; + ImageBuffer dstImg(canvasW, canvasH, PixelFormatIDs::RGBA8_Straight); + + NinePatchSourceNode ninePatch; + ninePatch.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + ninePatch.setOutputSize(60.0f, 20.0f); + + RendererNode renderer; + SinkNode sink(dstImg.view(), 0, 0); + + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasW, canvasH); + renderer.exec(); + + CHECK(hasNonZeroPixels(dstImg.view())); +} + +TEST_CASE("NinePatchSourceNode output smaller than fixed parts") { + // 出力サイズが固定部合計(10)より小さい場合のクリッピング + ImageBuffer srcImg = createSolidImage(20, 20, 255, 255, 0, 255); + + const int canvasSize = 8; + ImageBuffer dstImg(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + NinePatchSourceNode ninePatch; + ninePatch.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + ninePatch.setOutputSize(8.0f, 8.0f); // 固定部合計10より小さい + + RendererNode renderer; + SinkNode sink(dstImg.view(), 0, 0); + + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // クリッピングされても描画されること + CHECK(hasNonZeroPixels(dstImg.view())); +} + +TEST_CASE("NinePatchSourceNode from 9patch image pipeline") { + // setupFromNinePatch を使ったパイプラインテスト + ImageBuffer ninePatchImg = + createNinePatchImage(16, 16, 4, 11, 4, 11, 100, 150, 200); + + const int canvasSize = 48; + ImageBuffer dstImg(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight); + + NinePatchSourceNode ninePatch; + ninePatch.setupFromNinePatch(ninePatchImg.view()); + ninePatch.setOutputSize(48.0f, 48.0f); + + RendererNode renderer; + SinkNode sink(dstImg.view(), 0, 0); + + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + CHECK(hasNonZeroPixels(dstImg.view())); +} + +TEST_CASE("NinePatchSourceNode with position offset") { + ImageBuffer srcImg = createSolidImage(20, 20, 255, 0, 0, 255); + + const int canvasSize = 60; + ImageBuffer dstNoOffset(canvasSize, canvasSize, + PixelFormatIDs::RGBA8_Straight); + ImageBuffer dstWithOffset(canvasSize, canvasSize, + PixelFormatIDs::RGBA8_Straight); + + // オフセットなし + { + NinePatchSourceNode ninePatch; + ninePatch.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + ninePatch.setOutputSize(30.0f, 30.0f); + RendererNode renderer; + SinkNode sink(dstNoOffset.view(), 0, 0); + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + } + + // オフセットあり + { + NinePatchSourceNode ninePatch; + ninePatch.setupWithBounds(srcImg.view(), 5, 5, 5, 5); + ninePatch.setOutputSize(30.0f, 30.0f); + ninePatch.setPosition(10.0f, 10.0f); + RendererNode renderer; + SinkNode sink(dstWithOffset.view(), 0, 0); + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + } + + CHECK(hasNonZeroPixels(dstNoOffset.view())); + CHECK(hasNonZeroPixels(dstWithOffset.view())); + + // 位置が異なること + bool identical = true; + for (int y = 0; y < canvasSize && identical; y++) { + for (int x = 0; x < canvasSize && identical; x++) { + uint8_t r1, g1, b1, a1, r2, g2, b2, a2; + getPixelRGBA8(dstNoOffset.view(), x, y, r1, g1, b1, a1); + getPixelRGBA8(dstWithOffset.view(), x, y, r2, g2, b2, a2); + if (r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2) + identical = false; + } + } + CHECK_FALSE(identical); +} + +TEST_CASE("NinePatchSourceNode invalid source produces empty output") { + const int canvasSize = 32; + // ゼロ初期化して未描画の検出を可能にする + ImageBuffer dstImg(canvasSize, canvasSize, PixelFormatIDs::RGBA8_Straight, + InitPolicy::Zero); + + NinePatchSourceNode ninePatch; + // setupしない → sourceValid_ = false + ninePatch.setOutputSize(32.0f, 32.0f); + + RendererNode renderer; + SinkNode sink(dstImg.view(), 0, 0); + + ninePatch >> renderer >> sink; + renderer.setVirtualScreen(canvasSize, canvasSize); + renderer.exec(); + + // 何も描画されない + CHECK_FALSE(hasNonZeroPixels(dstImg.view())); +}