diff --git a/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/src/main/resources/data/computercraft/lua/rom/apis/colors.lua index 1edb47bce..83c8e0f21 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/colors.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/colors.lua @@ -332,3 +332,21 @@ function rgb8(r, g, b) return packRGB(r, g, b) end end + +-- Colour to hex lookup table for toBlit +local color_hex_lookup = {} +for i = 0, 15 do + color_hex_lookup[2 ^ i] = string.format("%x", i) +end + +--- Converts the given color to a paint/blit hex character (0-9a-f). +-- +-- This is equivalent to converting floor(log_2(color)) to hexadecimal. +-- +-- @tparam number color The color to convert. +-- @treturn string The blit hex code of the color. +function toBlit(color) + expect(1, color, "number") + return color_hex_lookup[color] or + string.format("%x", math.floor(math.log(color) / math.log(2))) +end diff --git a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua index 1af17ee6c..7ae1006b8 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua @@ -23,6 +23,25 @@ local function parseLine(tImageArg, sLine) table.insert(tImageArg, tLine) end +-- Sorts pairs of startX/startY/endX/endY such that the start is always the min +local function sortCoords(startX, startY, endX, endY) + local minX, maxX, minY, maxY + + if startX <= endX then + minX, maxX = startX, endX + else + minX, maxX = endX, startX + end + + if startY <= endY then + minY, maxY = startY, endY + else + minY, maxY = endY, startY + end + + return minX, maxX, minY, maxY +end + --- Parses an image from a multi-line string -- -- @tparam string image The string containing the raw-image data. @@ -71,9 +90,6 @@ function drawPixel(xPos, yPos, colour) expect(2, yPos, "number") expect(3, colour, "number", "nil") - if type(xPos) ~= "number" then error("bad argument #1 (expected number, got " .. type(xPos) .. ")", 2) end - if type(yPos) ~= "number" then error("bad argument #2 (expected number, got " .. type(yPos) .. ")", 2) end - if colour ~= nil and type(colour) ~= "number" then error("bad argument #3 (expected number, got " .. type(colour) .. ")", 2) end if colour then term.setBackgroundColor(colour) end @@ -111,17 +127,7 @@ function drawLine(startX, startY, endX, endY, colour) return end - local minX = math.min(startX, endX) - local maxX, minY, maxY - if minX == startX then - minY = startY - maxX = endX - maxY = endY - else - minY = endY - maxX = startX - maxY = startY - end + local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) -- TODO: clip to screen rectangle? @@ -177,37 +183,33 @@ function drawBox(startX, startY, endX, endY, nColour) endY = math.floor(endY) if nColour then - term.setBackgroundColor(nColour) + term.setBackgroundColor(nColour) -- Maintain legacy behaviour + else + nColour = term.getBackgroundColour() end + local colourHex = colours.toBlit(nColour) + if startX == endX and startY == endY then drawPixelInternal(startX, startY) return end - local minX = math.min(startX, endX) - local maxX, minY, maxY - if minX == startX then - minY = startY - maxX = endX - maxY = endY - else - minY = endY - maxX = startX - maxY = startY - end + local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) + local width = maxX - minX + 1 - for x = minX, maxX do - drawPixelInternal(x, minY) - drawPixelInternal(x, maxY) - end - - if maxY - minY >= 2 then - for y = minY + 1, maxY - 1 do - drawPixelInternal(minX, y) - drawPixelInternal(maxX, y) + for y = minY, maxY do + if y == minY or y == maxY then + term.setCursorPos(minX, y) + term.blit((" "):rep(width), colourHex:rep(width), colourHex:rep(width)) + else + term.setCursorPos(minX, y) + term.blit(" ", colourHex, colourHex) + term.setCursorPos(maxX, y) + term.blit(" ", colourHex, colourHex) end end end + --- Draws a filled box on the current term from the specified start position to -- the specified end position. -- @@ -233,29 +235,23 @@ function drawFilledBox(startX, startY, endX, endY, nColour) endY = math.floor(endY) if nColour then - term.setBackgroundColor(nColour) + term.setBackgroundColor(nColour) -- Maintain legacy behaviour + else + nColour = term.getBackgroundColour() end + local colourHex = colours.toBlit(nColour) + if startX == endX and startY == endY then drawPixelInternal(startX, startY) return end - local minX = math.min(startX, endX) - local maxX, minY, maxY - if minX == startX then - minY = startY - maxX = endX - maxY = endY - else - minY = endY - maxX = startX - maxY = startY - end + local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) + local width = maxX - minX + 1 - for x = minX, maxX do - for y = minY, maxY do - drawPixelInternal(x, y) - end + for y = minY, maxY do + term.setCursorPos(minX, y) + term.blit((" "):rep(width), colourHex:rep(width), colourHex:rep(width)) end end diff --git a/src/test/resources/test-rom/spec/apis/colors_spec.lua b/src/test/resources/test-rom/spec/apis/colors_spec.lua index 86b8adfe9..c7505ca5a 100644 --- a/src/test/resources/test-rom/spec/apis/colors_spec.lua +++ b/src/test/resources/test-rom/spec/apis/colors_spec.lua @@ -73,4 +73,20 @@ describe("The colors library", function() expect(colors.rgb8(0.3, 0.5, 0.6)):equals(0x4c7f99) expect({ colors.rgb8(0x4c7f99) }):same { 0x4c / 0xFF, 0x7f / 0xFF, 0.6 } end) + + describe("colors.toBlit", function() + it("validates arguments", function() + expect.error(colors.toBlit, nil):eq("bad argument #1 (expected number, got nil)") + end) + + it("converts all colors", function() + for i = 0, 15 do + expect(colors.toBlit(2 ^ i)):eq(string.format("%x", i)) + end + end) + + it("floors colors", function() + expect(colors.toBlit(16385)):eq("e") + end) + end) end) diff --git a/src/test/resources/test-rom/spec/apis/paintutils_spec.lua b/src/test/resources/test-rom/spec/apis/paintutils_spec.lua index fc72eece2..fc2b6008c 100644 --- a/src/test/resources/test-rom/spec/apis/paintutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/paintutils_spec.lua @@ -1,4 +1,19 @@ +local with_window = require "test_helpers".with_window + describe("The paintutils library", function() + -- Verifies that a window's lines are equal to the given table of blit + -- strings ({{"text", "fg", "bg"}, {"text", "fg", "bg"}...}) + local function window_eq(w, state) + -- Verification of the size isn't really important in the tests, but + -- better safe than sorry. + local _, height = w.getSize() + expect(#state):eq(height) + + for line = 1, height do + expect({ w.getLine(line) }):same(state[line]) + end + end + describe("paintutils.parseImage", function() it("validates arguments", function() paintutils.parseImage("") @@ -28,6 +43,30 @@ describe("The paintutils library", function() expect.error(paintutils.drawLine, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") expect.error(paintutils.drawLine, 1, 1, 1, 1, false):eq("bad argument #5 (expected number, got boolean)") end) + + it("draws a line going across with custom colour", function() + local w = with_window(3, 2, function() + paintutils.drawLine(1, 1, 3, 1, colours.red) + end) + + window_eq(w, { + { " ", "000", "eee" }, + { " ", "000", "fff" }, + }) + end) + + it("draws a line going diagonally with term colour", function() + local w = with_window(3, 3, function() + term.setBackgroundColour(colours.red) + paintutils.drawLine(1, 1, 3, 3) + end) + + window_eq(w, { + { " ", "000", "eff" }, + { " ", "000", "fef" }, + { " ", "000", "ffe" }, + }) + end) end) describe("paintutils.drawBox", function() @@ -38,6 +77,45 @@ describe("The paintutils library", function() expect.error(paintutils.drawBox, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") expect.error(paintutils.drawBox, 1, 1, 1, 1, false):eq("bad argument #5 (expected number, got boolean)") end) + + it("draws a box with term colour", function() + local w = with_window(3, 3, function() + term.setBackgroundColour(colours.red) + paintutils.drawBox(1, 1, 3, 3) + end) + + window_eq(w, { + { " ", "eee", "eee" }, + { " ", "e0e", "efe" }, + { " ", "eee", "eee" }, + }) + end) + + it("draws a box with custom colour", function() + local w = with_window(3, 3, function() + paintutils.drawBox(1, 1, 3, 3, colours.red) + end) + + window_eq(w, { + { " ", "eee", "eee" }, + { " ", "e0e", "efe" }, + { " ", "eee", "eee" }, + }) + end) + + it("draws a box without overwriting existing content", function() + local w = with_window(3, 3, function() + term.setCursorPos(2, 2) + term.write("a") + paintutils.drawBox(1, 1, 3, 3, colours.red) + end) + + window_eq(w, { + { " ", "eee", "eee" }, + { " a ", "e0e", "efe" }, + { " ", "eee", "eee" }, + }) + end) end) describe("paintutils.drawFilledBox", function() @@ -48,6 +126,31 @@ describe("The paintutils library", function() expect.error(paintutils.drawFilledBox, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") expect.error(paintutils.drawFilledBox, 1, 1, 1, 1, false):eq("bad argument #5 (expected number, got boolean)") end) + + it("draws a filled box with term colour", function() + local w = with_window(3, 3, function() + term.setBackgroundColour(colours.red) + paintutils.drawFilledBox(1, 1, 3, 3) + end) + + window_eq(w, { + { " ", "eee", "eee" }, + { " ", "eee", "eee" }, + { " ", "eee", "eee" }, + }) + end) + + it("draws a filled box with custom colour", function() + local w = with_window(3, 3, function() + paintutils.drawFilledBox(1, 1, 3, 3, colours.red) + end) + + window_eq(w, { + { " ", "eee", "eee" }, + { " ", "eee", "eee" }, + { " ", "eee", "eee" }, + }) + end) end) describe("paintutils.drawImage", function()