From ee31f401aac4673722763698f3148f0d1c21a7ac Mon Sep 17 00:00:00 2001 From: ShadelessFox Date: Sun, 19 Jan 2025 14:10:52 +0100 Subject: [PATCH 1/2] Add `canvas` UI node --- doc/en/scripting/ui.md | 19 +++ doc/en/xml-ui-layouts.md | 6 +- doc/ru/scripting/ui.md | 19 +++ doc/ru/xml-ui-layouts.md | 4 + src/graphics/ui/elements/Canvas.cpp | 19 +++ src/graphics/ui/elements/Canvas.hpp | 25 ++++ src/graphics/ui/gui_xml.cpp | 14 ++ src/logic/scripting/lua/libs/libgui.cpp | 9 ++ src/logic/scripting/lua/lua_custom_types.hpp | 23 +++ src/logic/scripting/lua/lua_engine.cpp | 1 + src/logic/scripting/lua/lua_util.hpp | 2 +- .../lua/usertypes/lua_type_canvas.cpp | 140 ++++++++++++++++++ 12 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 src/graphics/ui/elements/Canvas.cpp create mode 100644 src/graphics/ui/elements/Canvas.hpp create mode 100644 src/logic/scripting/lua/usertypes/lua_type_canvas.cpp diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index f5bb8401a..441115c8e 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -156,6 +156,25 @@ Properties: | ----- | ------ | ---- | ----- | ------------ | | src | string | yes | yes | texture name | +## Canvas + +Properties: + +| Title | Type | Read | Write | Description | +|-------|--------| ---- |-------|-------------| +| data | Canvas | yes | no | canvas data | + +Methods: + +| Method | Description | +|----------------------------------------------------------|---------------------------------------------------------| +| data:at(x: int, y: int) | returns an RGBA pixel at the given coordinates | +| data:set(x: int, y: int, rgba: int) | updates an RGBA pixel at the given coordinates | +| data:set(x: int, y: int, r: int, g: int, b: int) | updates an RGBA pixel at the given coordinates | +| data:set(x: int, y: int, r: int, g: int, b: int, a: int) | updates an RGBA pixel at the given coordinates | +| data:update() | applies changes to the canvas and uploads it to the GPU | + + ## Inventory Properties: diff --git a/doc/en/xml-ui-layouts.md b/doc/en/xml-ui-layouts.md index 8856338ad..6f687328d 100644 --- a/doc/en/xml-ui-layouts.md +++ b/doc/en/xml-ui-layouts.md @@ -97,7 +97,11 @@ Inner text is a button text. - `src` - name of an image stored in textures folder. Extension is not specified. Type: string. Example: *gui/error* - ## *textbox* +## *canvas* + +- _No additional attributes_ + +## *textbox* Inner text - initially entered text diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 7e10a1804..cca9790e2 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -156,6 +156,25 @@ document["worlds-panel"]:clear() | -------- | ------ | ------ | ------ | --------------------- | | src | string | да | да | отображаемая текстура | + +## Холст (canvas) + +Свойства: + +| Название | Тип | Чтение | Запись | Описание | +|----------|--------| ------ |--------|----------------| +| data | Canvas | да | нет | пиксели холста | + +Методы: + +| Метод | Описание | +|----------------------------------------------------------|-----------------------------------------------------| +| data:at(x: int, y: int) | возвращает RGBA пиксель по указанным координатам | +| data:set(x: int, y: int, rgba: int) | изменяет RGBA пиксель по указанным координатам | +| data:set(x: int, y: int, r: int, g: int, b: int) | изменяет RGBA пиксель по указанным координатам | +| data:set(x: int, y: int, r: int, g: int, b: int, a: int) | изменяет RGBA пиксель по указанным координатам | +| data:update() | применяет изменения и загружает холст в видеопамять | + ## Inventory (inventory) Свойства: diff --git a/doc/ru/xml-ui-layouts.md b/doc/ru/xml-ui-layouts.md index 00db07547..30c173156 100644 --- a/doc/ru/xml-ui-layouts.md +++ b/doc/ru/xml-ui-layouts.md @@ -98,6 +98,10 @@ - `src` - имя изображения в папке textures без указания расширения. Тип: строка. Например `gui/error` +## Холст - *canvas* + +- _Нет дополнительных свойств_ + ## Текстовое поле - *textbox* Внутренний текст - изначально введенный текст diff --git a/src/graphics/ui/elements/Canvas.cpp b/src/graphics/ui/elements/Canvas.cpp new file mode 100644 index 000000000..cf3c2bc6c --- /dev/null +++ b/src/graphics/ui/elements/Canvas.cpp @@ -0,0 +1,19 @@ +#include "Canvas.hpp" + +#include "graphics/core/Batch2D.hpp" +#include "graphics/core/DrawContext.hpp" +#include "graphics/core/Texture.hpp" + +gui::Canvas::Canvas(ImageFormat inFormat, glm::uvec2 inSize) : UINode(inSize) { + ImageData data {inFormat, inSize.x, inSize.y}; + mTexture = Texture::from(&data); +} + +void gui::Canvas::draw(const DrawContext& pctx, const Assets& assets) { + auto pos = calcPos(); + auto col = calcColor(); + + auto batch = pctx.getBatch2D(); + batch->texture(mTexture.get()); + batch->rect(pos.x, pos.y, size.x, size.y, 0, 0, 0, {}, false, true, col); +} diff --git a/src/graphics/ui/elements/Canvas.hpp b/src/graphics/ui/elements/Canvas.hpp new file mode 100644 index 000000000..2be9bf5ca --- /dev/null +++ b/src/graphics/ui/elements/Canvas.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "UINode.hpp" +#include "graphics/core/ImageData.hpp" +#include "graphics/core/Texture.hpp" + +class Texture; + +namespace gui { + class Canvas final : public UINode { + public: + explicit Canvas(ImageFormat inFormat, glm::uvec2 inSize); + + ~Canvas() override = default; + + void draw(const DrawContext& pctx, const Assets& assets) override; + + [[nodiscard]] std::shared_ptr<::Texture> texture() const { + return mTexture; + } + private: + std::shared_ptr<::Texture> mTexture; + std::unique_ptr mData; + }; +} \ No newline at end of file diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index 972b118ff..6bae272da 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -4,6 +4,7 @@ #include "elements/Image.hpp" #include "elements/Menu.hpp" #include "elements/Button.hpp" +#include "elements/Canvas.hpp" #include "elements/CheckBox.hpp" #include "elements/TextBox.hpp" #include "elements/TrackBar.hpp" @@ -455,6 +456,18 @@ static std::shared_ptr readImage( return image; } +static std::shared_ptr readCanvas( + const UiXmlReader& reader, const xml::xmlelement& element +) { + auto size = glm::uvec2{32, 32}; + if (element.has("size")) { + size = element.attr("size").asVec2(); + } + auto image = std::make_shared(ImageFormat::rgba8888, size); + _readUINode(reader, element, *image); + return image; +} + static std::shared_ptr readTrackBar( const UiXmlReader& reader, const xml::xmlelement& element ) { @@ -634,6 +647,7 @@ static std::shared_ptr readPageBox(UiXmlReader& reader, const xml::xmlel UiXmlReader::UiXmlReader(const scriptenv& env) : env(env) { contextStack.emplace(""); add("image", readImage); + add("canvas", readCanvas); add("label", readLabel); add("panel", readPanel); add("button", readButton); diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 2947d80c9..995cb625d 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -3,6 +3,7 @@ #include "engine/Engine.hpp" #include "frontend/locale.hpp" #include "graphics/ui/elements/Button.hpp" +#include "graphics/ui/elements/Canvas.hpp" #include "graphics/ui/elements/CheckBox.hpp" #include "graphics/ui/elements/Image.hpp" #include "graphics/ui/elements/InventoryView.hpp" @@ -323,6 +324,13 @@ static int p_get_src(UINode* node, lua::State* L) { return 0; } +static int p_get_data(UINode* node, lua::State* L) { + if (auto canvas = dynamic_cast(node)) { + return lua::newuserdata(L, canvas->texture()); + } + return 0; +} + static int p_get_add(UINode* node, lua::State* L) { if (dynamic_cast(node)) { return lua::pushcfunction(L, lua::wrap); @@ -464,6 +472,7 @@ static int l_gui_getattr(lua::State* L) { {"inventory", p_get_inventory}, {"focused", p_get_focused}, {"cursor", p_get_cursor}, + {"data", p_get_data}, }; auto func = getters.find(attr); if (func != getters.end()) { diff --git a/src/logic/scripting/lua/lua_custom_types.hpp b/src/logic/scripting/lua/lua_custom_types.hpp index bb8037ee1..56a3c83c5 100644 --- a/src/logic/scripting/lua/lua_custom_types.hpp +++ b/src/logic/scripting/lua/lua_custom_types.hpp @@ -8,6 +8,8 @@ struct fnl_state; class Heightmap; class VoxelFragment; +class Texture; +class ImageData; namespace lua { class Userdata { @@ -90,4 +92,25 @@ namespace lua { inline static std::string TYPENAME = "VoxelFragment"; }; static_assert(!std::is_abstract()); + + class LuaCanvas : public Userdata { + public: + explicit LuaCanvas(std::shared_ptr inTexture); + ~LuaCanvas() override = default; + + const std::string& getTypeName() const override { + return TYPENAME; + } + + [[nodiscard]] Texture& texture() const { return *mTexture; } + + [[nodiscard]] ImageData& data() const { return *mData; } + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "Canvas"; + private: + std::shared_ptr mTexture; + std::unique_ptr mData; + }; + static_assert(!std::is_abstract()); } diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 39cb1b8f0..1f4bde7d2 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -115,6 +115,7 @@ void lua::init_state(State* L, StateType stateType) { newusertype(L); newusertype(L); newusertype(L); + newusertype(L); } void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index 1baf9c357..9539fbbf0 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -274,7 +274,7 @@ namespace lua { inline int newuserdata(lua::State* L, Args&&... args) { const auto& found = usertypeNames.find(typeid(T)); void* ptr = lua_newuserdata(L, sizeof(T)); - new (ptr) T(args...); + new (ptr) T(std::forward(args)...); if (found == usertypeNames.end()) { log_error( diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp new file mode 100644 index 000000000..2d3ad58c7 --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -0,0 +1,140 @@ +#include + +#include "graphics/core/ImageData.hpp" +#include "graphics/core/Texture.hpp" +#include "logic/scripting/lua/lua_custom_types.hpp" +#include "logic/scripting/lua/lua_util.hpp" + +using namespace lua; + +LuaCanvas::LuaCanvas(std::shared_ptr inTexture) + : mTexture(std::move(inTexture)) { + mData = mTexture->readData(); +} + +union RGBA { + uint8_t rgba[4]; + uint32_t raw; +}; + +static RGBA* get_at(const ImageData& data, uint index) { + if (index >= data.getWidth() * data.getHeight()) { + return nullptr; + } + 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); +} + +static RGBA* get_at(State* L, uint x, uint y) { + if (auto texture = touserdata(L, 1)) { + return get_at(texture->data(), x, y); + } + return nullptr; +} + +static int l_at(State* L) { + auto x = static_cast(tonumber(L, 2)); + auto y = static_cast(tonumber(L, 3)); + + if (auto pixel = get_at(L, x, y)) { + return pushnumber(L, pixel->raw); + } + + return 0; +} + +static int l_set(State* L) { + auto x = static_cast(tonumber(L, 2)); + auto y = static_cast(tonumber(L, 3)); + + if (auto pixel = get_at(L, x, y)) { + switch (gettop(L)) { + case 4: + pixel->raw = static_cast(tonumber(L, 4)); + return 1; + case 6: + pixel->rgba[0] = static_cast(tonumber(L, 4)); + pixel->rgba[1] = static_cast(tonumber(L, 5)); + pixel->rgba[2] = static_cast(tonumber(L, 6)); + pixel->rgba[3] = 255; + return 1; + case 7: + pixel->rgba[0] = static_cast(tonumber(L, 4)); + pixel->rgba[1] = static_cast(tonumber(L, 5)); + pixel->rgba[2] = static_cast(tonumber(L, 6)); + pixel->rgba[3] = static_cast(tonumber(L, 7)); + return 1; + default: + return 0; + } + } + + return 0; +} + +static int l_update(State* L) { + if (auto texture = touserdata(L, 1)) { + texture->texture().reload(texture->data()); + } + return 0; +} + +static std::unordered_map methods { + {"at", lua::wrap}, + {"set", lua::wrap}, + {"update", lua::wrap} +}; + +static int l_meta_index(State* L) { + auto texture = touserdata(L, 1); + if (texture == nullptr) { + return 0; + } + auto& data = texture->data(); + if (isnumber(L, 2)) { + if (auto pixel = get_at(data, static_cast(tonumber(L, 2)))) { + return pushinteger(L, pixel->raw); + } + } + if (isstring(L, 2)) { + auto name = tostring(L, 2); + if (!strcmp(name, "width")) { + return pushinteger(L, data.getWidth()); + } + if (!strcmp(name, "height")) { + return pushinteger(L, data.getHeight()); + } + if (auto func = methods.find(tostring(L, 2)); func != methods.end()) { + return pushcfunction(L, func->second); + } + } + return 0; +} + +static int l_meta_newindex(State* L) { + auto texture = touserdata(L, 1); + if (texture == nullptr) { + return 0; + } + auto& data = texture->data(); + if (isnumber(L, 2) && isnumber(L, 3)) { + if (auto pixel = get_at(data, static_cast(tonumber(L, 2)))) { + pixel->raw = static_cast(tonumber(L, 3)); + return 1; + } + return 1; + } + return 0; +} + +int LuaCanvas::createMetatable(State* L) { + createtable(L, 0, 3); + pushcfunction(L, lua::wrap); + setfield(L, "__index"); + pushcfunction(L, lua::wrap); + setfield(L, "__newindex"); + return 1; +} From 5ee44a02f90bbd39d3bca5ca34dd3109bbeb01ea Mon Sep 17 00:00:00 2001 From: ShadelessFox Date: Sun, 19 Jan 2025 18:21:48 +0100 Subject: [PATCH 2/2] Use `integer` instead of `number` function variants --- .../lua/usertypes/lua_type_canvas.cpp | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index 2d3ad58c7..b9a205147 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -36,36 +36,36 @@ static RGBA* get_at(State* L, uint x, uint y) { } static int l_at(State* L) { - auto x = static_cast(tonumber(L, 2)); - auto y = static_cast(tonumber(L, 3)); + auto x = static_cast(tointeger(L, 2)); + auto y = static_cast(tointeger(L, 3)); if (auto pixel = get_at(L, x, y)) { - return pushnumber(L, pixel->raw); + return pushinteger(L, pixel->raw); } return 0; } static int l_set(State* L) { - auto x = static_cast(tonumber(L, 2)); - auto y = static_cast(tonumber(L, 3)); + auto x = static_cast(tointeger(L, 2)); + auto y = static_cast(tointeger(L, 3)); if (auto pixel = get_at(L, x, y)) { switch (gettop(L)) { case 4: - pixel->raw = static_cast(tonumber(L, 4)); + pixel->raw = static_cast(tointeger(L, 4)); return 1; case 6: - pixel->rgba[0] = static_cast(tonumber(L, 4)); - pixel->rgba[1] = static_cast(tonumber(L, 5)); - pixel->rgba[2] = static_cast(tonumber(L, 6)); + pixel->rgba[0] = static_cast(tointeger(L, 4)); + pixel->rgba[1] = static_cast(tointeger(L, 5)); + pixel->rgba[2] = static_cast(tointeger(L, 6)); pixel->rgba[3] = 255; return 1; case 7: - pixel->rgba[0] = static_cast(tonumber(L, 4)); - pixel->rgba[1] = static_cast(tonumber(L, 5)); - pixel->rgba[2] = static_cast(tonumber(L, 6)); - pixel->rgba[3] = static_cast(tonumber(L, 7)); + pixel->rgba[0] = static_cast(tointeger(L, 4)); + pixel->rgba[1] = static_cast(tointeger(L, 5)); + pixel->rgba[2] = static_cast(tointeger(L, 6)); + pixel->rgba[3] = static_cast(tointeger(L, 7)); return 1; default: return 0; @@ -95,7 +95,7 @@ static int l_meta_index(State* L) { } auto& data = texture->data(); if (isnumber(L, 2)) { - if (auto pixel = get_at(data, static_cast(tonumber(L, 2)))) { + if (auto pixel = get_at(data, static_cast(tointeger(L, 2)))) { return pushinteger(L, pixel->raw); } } @@ -121,8 +121,8 @@ static int l_meta_newindex(State* L) { } auto& data = texture->data(); if (isnumber(L, 2) && isnumber(L, 3)) { - if (auto pixel = get_at(data, static_cast(tonumber(L, 2)))) { - pixel->raw = static_cast(tonumber(L, 3)); + if (auto pixel = get_at(data, static_cast(tointeger(L, 2)))) { + pixel->raw = static_cast(tointeger(L, 3)); return 1; } return 1;