From 279fb92b94a0f8b8d28ed141b54b160450ec91f7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:14:38 +0300 Subject: [PATCH 1/5] fix canvas:encode produces flipped images for png --- src/coders/png.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coders/png.cpp b/src/coders/png.cpp index 030844352..18cc0a474 100644 --- a/src/coders/png.cpp +++ b/src/coders/png.cpp @@ -49,9 +49,9 @@ static util::Buffer write_to_memory(uint width, uint height, const ubyte* png_write_info(png_ptr, info_ptr); auto row = std::make_unique(pixsize * width); - for (uint y = 0; y < height; y++) { - for (uint x = 0; x < width; x++) { - for (uint i = 0; i < pixsize; i++) { + for (int y = height-1; y >= 0; y--) { + for (int x = 0; x < width; x++) { + for (int i = 0; i < pixsize; i++) { row[x * pixsize + i] = (png_byte)data[(y * width + x) * pixsize + i]; } From a5596ed5cd65bc62b983ec9dff3263beeced2dd7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:25:04 +0300 Subject: [PATCH 2/5] fix canvas:line lines clipping --- src/graphics/core/ImageData.cpp | 121 +++++++++++++----- .../lua/usertypes/lua_type_canvas.cpp | 11 +- 2 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/graphics/core/ImageData.cpp b/src/graphics/core/ImageData.cpp index 6875d7ab9..5f7761188 100644 --- a/src/graphics/core/ImageData.cpp +++ b/src/graphics/core/ImageData.cpp @@ -1,5 +1,7 @@ #include "ImageData.hpp" +#include "debug/Logger.hpp" + #include #include #include @@ -7,6 +9,8 @@ #include #include +static debug::Logger logger("image-data"); + ImageData::ImageData(ImageFormat format, uint width, uint height) : format(format), width(width), height(height) { size_t pixsize; @@ -106,47 +110,83 @@ std::unique_ptr ImageData::cropped(int x, int y, int width, int heigh return subImage; } -static bool clip_line(int& x1, int& y1, int& x2, int& y2, int width, int height) { - const int left = 0; - const int right = width; - const int bottom = 0; - const int top = height; +static bool clip_line( + int& x0, int& y0, int& x1, int& y1, int width, int height +) { + if (width <= 0 || height <= 0) { + return false; + } + constexpr int INSIDE = 0; + constexpr int LEFT = 1; + constexpr int RIGHT = 2; + constexpr int BOTTOM = 4; + constexpr int TOP = 8; - int dx = x2 - x1; - int dy = y2 - y1; + const int xmin = 0; + const int ymin = 0; + const int xmax = width - 1; + const int ymax = height - 1; - float t0 = 0.0f; - float t1 = 1.0f; + auto outcode = [&](int x, int y) { + int code = INSIDE; - auto clip = [](int p, int q, float& t0, float& t1) { - if (p == 0) { - return q >= 0; - } - float t = static_cast(q) / p; - if (p < 0) { - if (t > t1) return false; - if (t > t0) t0 = t; - } else { - if (t < t0) return false; - if (t < t1) t1 = t; - } - return true; + if (x < xmin) + code |= LEFT; + else if (x > xmax) + code |= RIGHT; + + if (y < ymin) + code |= BOTTOM; + else if (y > ymax) + code |= TOP; + + return code; }; - if (!clip(-dx, x1 - left, t0, t1)) return false; - if (!clip( dx, right - x1, t0, t1)) return false; - if (!clip(-dy, y1 - bottom, t0, t1)) return false; - if (!clip( dy, top - y1, t0, t1)) return false; + int c0 = outcode(x0, y0); + int c1 = outcode(x1, y1); - if (t1 < 1.0f) { - x2 = x1 + static_cast(std::round(t1 * dx)); - y2 = y1 + static_cast(std::round(t1 * dy)); + while (true) { + if (!(c0 | c1)) { + return true; + } + if (c0 & c1) { + return false; + } + + const int out = c0 ? c0 : c1; + + double x = 0.0; + double y = 0.0; + + if (out & TOP) { + y = ymax; + x = x0 + (x1 - x0) * static_cast(ymax - y0) / (y1 - y0); + } else if (out & BOTTOM) { + y = ymin; + x = x0 + (x1 - x0) * static_cast(ymin - y0) / (y1 - y0); + } else if (out & RIGHT) { + x = xmax; + y = y0 + (y1 - y0) * static_cast(xmax - x0) / (x1 - x0); + } else { // LEFT + x = xmin; + y = y0 + (y1 - y0) * static_cast(xmin - x0) / (x1 - x0); + } + + if (out == c0) { + x0 = std::clamp(static_cast(std::lround(x)), xmin, xmax); + y0 = std::clamp(static_cast(std::lround(y)), ymin, ymax); + c0 = outcode(x0, y0); + } else { + x1 = std::clamp(static_cast(std::lround(x)), xmin, xmax); + y1 = std::clamp(static_cast(std::lround(y)), ymin, ymax); + c1 = outcode(x1, y1); + } } - if (t0 > 0.0f) { - x1 = x1 + static_cast(std::round(t0 * dx)); - y1 = y1 + static_cast(std::round(t0 * dy)); - } - return true; +} + +static bool is_point_outside(int x, int y, int width, int height) { + return x < 0 || y < 0 || x >= width || y >= height; } template @@ -155,11 +195,24 @@ static void draw_line(ImageData& image, int x1, int y1, int x2, int y2, const gl uint width = image.getWidth(); uint height = image.getHeight(); + glm::ivec4 init {x1, y1, x2, y2}; + if ((x1 < 0 || x1 >= width || x2 < 0 || x2 >= width || y1 < 0 || y1 >= height || y2 < 0 || y2 >= height) && !clip_line(x1, y1, x2, y2, width, height)) { return; } + // spam info for bug report + if (is_point_outside(x1, y1, width, height) || is_point_outside(x2, y2, width, height)) { + logger.warning() << "clip_line fault: [" << init.x << ", " << init.y + << "] - [" << init.z << ", " << init.w << "] --> [" + << x1 << ", " << y1 << "] - [" << x2 << ", " << y2 + << "]"; + x1 = glm::clamp(x1, 0, static_cast(width) - 1); + y1 = glm::clamp(y1, 0, static_cast(height) - 1); + x2 = glm::clamp(x2, 0, static_cast(width) - 1); + y2 = glm::clamp(y2, 0, static_cast(height) - 1); + } int dx = std::abs(x2 - x1); int dy = -std::abs(y2 - y1); diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index 37c53abd9..c670e23df 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -81,16 +81,17 @@ union RGBA { uint8_t arr[4]; uint32_t rgba; }; +static_assert(sizeof(RGBA) == 4); -static RGBA* get_at(const ImageData& data, uint index) { - if (index >= data.getWidth() * data.getHeight()) { - return nullptr; - } +static RGBA* get_at(const ImageData& data, size_t index) { return reinterpret_cast(data.getData() + index * sizeof(RGBA)); } static RGBA* get_at(const ImageData& data, uint x, uint y) { - return get_at(data, y * data.getWidth() + x); + if (x >= data.getWidth() || y >= data.getHeight()) { + return nullptr; + } + return get_at(data, static_cast(y) * data.getWidth() + x); } static RGBA* get_at(State* L, uint x, uint y) { From 586ab3948323e2160ac10198c97528d891ab05d8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:26:49 +0300 Subject: [PATCH 3/5] add canvas comparsion to asserts.equals --- res/modules/internal/asserts.lua | 73 +++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua index 2317bb7ce..5c52de875 100644 --- a/res/modules/internal/asserts.lua +++ b/res/modules/internal/asserts.lua @@ -1,8 +1,77 @@ local this = {} +local app = __vc_app -function this.equals(expected, fact) +local function diff_canvas(expected, fact) + this.equals(expected.width, fact.width, "image width") + this.equals(expected.width, fact.width, "image height") + + local diff = Canvas({expected.width, expected.height}) + + local mismatches = 0 + + for y = 0, expected.height - 1 do + for x = 0, expected.width - 1 do + local ca = expected:at(x, y) + local cb = fact:at(x, y) + + if ca == cb then + diff:set(x, y, ca) + else + diff:set(x, y, 0xff0000ff) + mismatches = mismatches + 1 + end + end + end + + return diff, mismatches +end + +local TEST_ARTIFACTS_PATH = "export:test-results" + +local function get_test_artifact_path(name) + local path = TEST_ARTIFACTS_PATH + if not file.isdir(path) then + file.mkdirs(path) + end + return file.join(path, name) +end + +function this.equals(expected, fact, prefix) + local mt = getmetatable(expected) + if mt == Canvas then + local diff, mismatches = diff_canvas(expected, fact) + if mismatches > 0 then + local test_name = app.script or random.uuid() + local path_diff = get_test_artifact_path( + string.format("diff_%s.png", test_name) + ) + local path_fact = get_test_artifact_path( + string.format("fact_%s.png", test_name) + ) + local path_expected = get_test_artifact_path( + string.format("expected_%s.png", test_name) + ) + file.write_bytes(path_diff, diff:encode('png')) + file.write_bytes(path_fact, fact:encode('png')) + file.write_bytes(path_expected, expected:encode('png')) + local total_pixels = expected.width * expected.height + assert(mismatches == 0, string.format( + "%s pixels of %s (%s%%) are different, diff saved as %s", + mismatches, + total_pixels, + math.round(mismatches / total_pixels * 100), + string.escape(path_diff)) + ) + end + return + end + if prefix then + prefix = prefix .. " " + else + prefix = "" + end assert(fact == expected, string.format( - "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", + prefix .. "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", expected, fact )) end From 917806a7ef8f67dee14a6a258ab3e9ae357a89d3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 13:27:27 +0300 Subject: [PATCH 4/5] add canvas lines test --- dev/tests/attachments/line_clipping_test.png | Bin 0 -> 1523 bytes dev/tests/canvas_lines.lua | 51 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 dev/tests/attachments/line_clipping_test.png create mode 100644 dev/tests/canvas_lines.lua diff --git a/dev/tests/attachments/line_clipping_test.png b/dev/tests/attachments/line_clipping_test.png new file mode 100644 index 0000000000000000000000000000000000000000..bb20d18415b55895486fc5ec65ebfcb54c2d6b6f GIT binary patch literal 1523 zcmVK^PVmfgrKJFx)a61v5=>Eu2#vd395Ypb#UO$uAVZ0toAT&1Kdw=%Z zb6$G%-#yQqCyU1~JbRD*?7jc;-uu4q>6~Ngd)B6N9aBHO{M=nS`tR4DOUt|C*N{EO z-Z{qzoWc*Le+W`UOZlzl{>^^6|3wM}R=(^!kMyVKKiGr7%9owFY5oNOY(rq>%g(!V zyjcLS4uO>~J4+|{Zvj9Yg7JoxFF2pB?iK;SG6Hw$7_Qq7&T?#GF6}=8fO!Nq?8{E< zx=s`Tctl|3%g&lby+3Q7I3VaZtQcDIWbKM>^auby@BEZ2hL*J1vYa0c;C|RQ1oE== zUQQH%xjEku$jR37Ap*eC3E$5@9XI`BOPcH{AK>eJ_Ws>3$MeAO--m+70T^M}d<#CbUTVn{;C)X|aEC9d|tVz!O7TExRA&5!t%{Ey9 zfFW2)iJey20e~TxOQ~&kMF4;yxKGJFmc;;oA%OB5Y>NVbwh@#r9|Qn^HW5fDAA|sa zwh%}u9|Qq_HV{ZEAA|vbx)De#9|Qt`IuS@LAA|ycx)4Y$9|Qw{IuJ-MAA|z{-3X+Y z4+;Q)P6Qgt2PFVN7XmHigCYQ+1A(UUK^Xv$jX+!Zpb!AaM4+*JPznHKA<$YrCAgHN)s0;uci=ejhp+W#~41yZV zhe`oJS_HM04;2G|Gze-gA1Vg``w-M#J~RLTwjpStd}ss!tV7UB`Opvmh(pj)`Op{u zSVquV`OqK$m`BiJ`Oqi;ctp@@`S9W6;-jMZHC;? z;CBSjdi;A|d*|G`Zw;#{Vc73nhojvopviN9=Q&@(*0NG*4#2w;zE%-51>n=!ev1g2 z0T6qs*BXK*0Ia>vZwW#50Jbjjw1S{&0DD*XT0l@OfKCfLyn9YeP^4fZX@)?AP@~eYxyA> z5Qqh!z5EdA2t)#?rTh@d2*d%Xt^5$F2t)y>wfqo?2*d!Wz5Ebq2t)vArTh>{2#Nz} zt^5!v2#Nw|wfqnX2#Nt{z5I~U2#Nr(@*h(YL3RLE{(MRy$O^#9Urq@G*#KDiamkG! z3jiyBU2-Bg9DtR-Ex8aJ3c$+Wm%|aPX9gO7#KFb&T}JkG5}V7p4=mVA^LIersX74 zfR&#&{rALp8-TcvieEY>OYnUGtbr&miofq*JP!=laq;#4^nD0l-h03G`vSUu!_d@E zn;tJAo$J&df9E>J8w}TRb_D)(HRhb#J_XpYFC$?(c)mJ_y%gVc5@_Yi%JbF1nsu1f z0IYnOxks>ON?$bvSo!iZHytb`dIYfYW#@j_@2>HWiy8r}eA#)P^QXR#9tA!Eu<~W+ z-3ebTf<*vUzU+)Q7SHx$5WEFo<;#w{bPN%5{U-#o0IYo3S-an23Bd~hD_?fDZnk-c z-~oV@FFSj8TFoK&1z_dNPO5Eo&j?On Date: Sat, 18 Jul 2026 13:54:50 +0300 Subject: [PATCH 5/5] update asserts --- res/modules/internal/asserts.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua index 5c52de875..b84d5d37f 100644 --- a/res/modules/internal/asserts.lua +++ b/res/modules/internal/asserts.lua @@ -41,15 +41,15 @@ function this.equals(expected, fact, prefix) if mt == Canvas then local diff, mismatches = diff_canvas(expected, fact) if mismatches > 0 then - local test_name = app.script or random.uuid() + prefix = prefix or app.script or base64.encode_urlsafe(random.bytes(6)) local path_diff = get_test_artifact_path( - string.format("diff_%s.png", test_name) + string.format("%s.diff.png", prefix) ) local path_fact = get_test_artifact_path( - string.format("fact_%s.png", test_name) + string.format("%s.fact.png", prefix) ) local path_expected = get_test_artifact_path( - string.format("expected_%s.png", test_name) + string.format("%s.expected.png", prefix) ) file.write_bytes(path_diff, diff:encode('png')) file.write_bytes(path_fact, fact:encode('png')) @@ -59,7 +59,7 @@ function this.equals(expected, fact, prefix) "%s pixels of %s (%s%%) are different, diff saved as %s", mismatches, total_pixels, - math.round(mismatches / total_pixels * 100), + math.round(mismatches / total_pixels * 100, 2), string.escape(path_diff)) ) end