diff --git a/dev/tests/attachments/line_clipping_test.png b/dev/tests/attachments/line_clipping_test.png new file mode 100644 index 000000000..bb20d1841 Binary files /dev/null and b/dev/tests/attachments/line_clipping_test.png differ diff --git a/dev/tests/canvas_lines.lua b/dev/tests/canvas_lines.lua new file mode 100644 index 000000000..c7ef68520 --- /dev/null +++ b/dev/tests/canvas_lines.lua @@ -0,0 +1,51 @@ +local W, H = 128, 128 + +local canvas = Canvas({W, H}) +canvas:clear(0xff202020) + +local WHITE = 0xffffffff +local RED = 0xffff4040 +local GREEN = 0xff40ff40 +local BLUE = 0xff4080ff +local YELLOW = 0xffffff40 + +-- border +canvas:line(0, 0, W - 1, 0, WHITE) +canvas:line(W - 1, 0, W - 1, H - 1, WHITE) +canvas:line(W - 1, H - 1, 0, H - 1, WHITE) +canvas:line(0, H - 1, 0, 0, WHITE) + +-- horizontal +canvas:line(-40, 16, W + 40, 16, RED) +canvas:line(-40, H / 2, W + 40, H / 2, RED) +canvas:line(-40, H - 17, W + 40, H - 17, RED) + +-- vertical +canvas:line(16, -40, 16, H + 40, GREEN) +canvas:line(W / 2, -40, W / 2, H + 40, GREEN) +canvas:line(W - 17, -40, W - 17, H + 40, GREEN) + +-- diagonals +canvas:line(-40, -40, W + 40, H + 40, BLUE) +canvas:line(-40, H + 40, W + 40, -40, BLUE) + +canvas:line(W / 2, -40, W + 40, H / 2, BLUE) +canvas:line(-40, H / 2, W / 2, H + 40, BLUE) + +-- outer +canvas:line(-30, -30, 20, 20, YELLOW) +canvas:line(W - 20, H - 20, W + 30, H + 30, YELLOW) + +canvas:line(W + 30, -30, W - 20, 20, YELLOW) +canvas:line(-30, H + 30, 20, H - 20, YELLOW) + +-- completely outside +canvas:line(-50, -20, -10, H + 20, 0xffff00ff) +canvas:line(W + 10, -20, W + 50, H + 20, 0xffff00ff) +canvas:line(-20, -20, W + 20, -10, 0xffff00ff) +canvas:line(-20, H + 10, W + 20, H + 30, 0xffff00ff) + +local expected = Canvas.decode( + file.read_bytes("script:attachments/line_clipping_test.png"), "png" +) +asserts.equals(expected, canvas) diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua index 2317bb7ce..b84d5d37f 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 + prefix = prefix or app.script or base64.encode_urlsafe(random.bytes(6)) + local path_diff = get_test_artifact_path( + string.format("%s.diff.png", prefix) + ) + local path_fact = get_test_artifact_path( + string.format("%s.fact.png", prefix) + ) + local path_expected = get_test_artifact_path( + string.format("%s.expected.png", prefix) + ) + 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, 2), + 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 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]; } 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) {