Phase 4.2: Add GridView UIDrawable type (addresses #252)
GridView is a new UIDrawable that renders a GridData object independently. Multiple GridViews can reference the same Grid for split-screen, minimap, or different camera/zoom perspectives on shared grid state. - New files: UIGridView.h/cpp with full rendering pipeline (copied from UIGrid::render, adapted to use grid_data pointer) - Add UIGRIDVIEW to PyObjectsEnum - Add UIGRIDVIEW cases to all switch(derived_type()) sites: Animation.cpp, UICollection.cpp, UIDrawable.cpp, McRFPy_API.cpp, ImGuiSceneExplorer.cpp - Python type: mcrfpy.GridView(grid=, pos=, size=, zoom=, fill_color=) - Properties: grid, center, zoom, fill_color, texture - Register type with metaclass for callback support All 258 existing tests pass. New GridView test suite added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13d5512a41
commit
4b13e5f5db
10 changed files with 824 additions and 2 deletions
|
|
@ -11,6 +11,7 @@
|
||||||
#include "UICaption.h"
|
#include "UICaption.h"
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
#include "UILine.h"
|
#include "UILine.h"
|
||||||
#include "UICircle.h"
|
#include "UICircle.h"
|
||||||
#include "UIArc.h"
|
#include "UIArc.h"
|
||||||
|
|
@ -557,6 +558,17 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
||||||
obj = (PyObject*)pyObj;
|
obj = (PyObject*)pyObj;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PyObjectsEnum::UIGRIDVIEW:
|
||||||
|
{
|
||||||
|
type = &mcrfpydef::PyUIGridViewType;
|
||||||
|
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||||
|
if (pyObj) {
|
||||||
|
pyObj->data = std::static_pointer_cast<UIGridView>(drawable);
|
||||||
|
pyObj->weakreflist = NULL;
|
||||||
|
}
|
||||||
|
obj = (PyObject*)pyObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case PyObjectsEnum::UILINE:
|
case PyObjectsEnum::UILINE:
|
||||||
{
|
{
|
||||||
type = &mcrfpydef::PyUILineType;
|
type = &mcrfpydef::PyUILineType;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
#include "UICaption.h"
|
#include "UICaption.h"
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "ImGuiConsole.h"
|
#include "ImGuiConsole.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
|
@ -285,6 +286,7 @@ const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) {
|
||||||
case PyObjectsEnum::UICAPTION: return "Caption";
|
case PyObjectsEnum::UICAPTION: return "Caption";
|
||||||
case PyObjectsEnum::UISPRITE: return "Sprite";
|
case PyObjectsEnum::UISPRITE: return "Sprite";
|
||||||
case PyObjectsEnum::UIGRID: return "Grid";
|
case PyObjectsEnum::UIGRID: return "Grid";
|
||||||
|
case PyObjectsEnum::UIGRIDVIEW: return "GridView";
|
||||||
case PyObjectsEnum::UILINE: return "Line";
|
case PyObjectsEnum::UILINE: return "Line";
|
||||||
case PyObjectsEnum::UICIRCLE: return "Circle";
|
case PyObjectsEnum::UICIRCLE: return "Circle";
|
||||||
case PyObjectsEnum::UIARC: return "Arc";
|
case PyObjectsEnum::UIARC: return "Arc";
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
#include "PyInputState.h"
|
#include "PyInputState.h"
|
||||||
#include "PyBehavior.h"
|
#include "PyBehavior.h"
|
||||||
#include "PyTrigger.h"
|
#include "PyTrigger.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
#include "PySound.h"
|
#include "PySound.h"
|
||||||
#include "PySoundBuffer.h"
|
#include "PySoundBuffer.h"
|
||||||
#include "PyMusic.h"
|
#include "PyMusic.h"
|
||||||
|
|
@ -465,6 +466,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
PyTypeObject* ui_types_with_callbacks[] = {
|
PyTypeObject* ui_types_with_callbacks[] = {
|
||||||
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyUIGridType,
|
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyUIGridType,
|
||||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||||
|
&mcrfpydef::PyUIGridViewType,
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
for (int i = 0; ui_types_with_callbacks[i] != nullptr; i++) {
|
for (int i = 0; ui_types_with_callbacks[i] != nullptr; i++) {
|
||||||
|
|
@ -482,6 +484,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*UI widgets*/
|
/*UI widgets*/
|
||||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
||||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||||
|
&mcrfpydef::PyUIGridViewType,
|
||||||
|
|
||||||
/*3D entities*/
|
/*3D entities*/
|
||||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||||
|
|
@ -635,6 +638,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
|
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
|
||||||
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
|
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
|
||||||
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
|
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
|
||||||
|
mcrfpydef::PyUIGridViewType.tp_weaklistoffset = offsetof(PyUIGridViewObject, weakreflist);
|
||||||
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
|
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
|
||||||
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
||||||
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
||||||
|
|
@ -1596,6 +1600,16 @@ static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collect
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PyObjectsEnum::UIGRIDVIEW: {
|
||||||
|
auto gridview = std::static_pointer_cast<UIGridView>(drawable);
|
||||||
|
auto type = &mcrfpydef::PyUIGridViewType;
|
||||||
|
auto o = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||||
|
if (o) {
|
||||||
|
o->data = gridview;
|
||||||
|
py_obj = (PyObject*)o;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "PyCallable.h"
|
#include "PyCallable.h"
|
||||||
#include "UIFrame.h"
|
#include "UIFrame.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
||||||
#include "PythonObjectCache.h" // #184 - For subclass callback support
|
#include "PythonObjectCache.h" // #184 - For subclass callback support
|
||||||
#include "McRFPy_API.h" // For Vector type access
|
#include "McRFPy_API.h" // For Vector type access
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "UICaption.h"
|
#include "UICaption.h"
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
#include "UILine.h"
|
#include "UILine.h"
|
||||||
#include "UICircle.h"
|
#include "UICircle.h"
|
||||||
#include "UIArc.h"
|
#include "UIArc.h"
|
||||||
|
|
@ -78,6 +79,17 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
||||||
obj = (PyObject*)pyObj;
|
obj = (PyObject*)pyObj;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PyObjectsEnum::UIGRIDVIEW:
|
||||||
|
{
|
||||||
|
type = &PyUIGridViewType;
|
||||||
|
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||||
|
if (pyObj) {
|
||||||
|
pyObj->data = std::static_pointer_cast<UIGridView>(drawable);
|
||||||
|
pyObj->weakreflist = NULL;
|
||||||
|
}
|
||||||
|
obj = (PyObject*)pyObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case PyObjectsEnum::UILINE:
|
case PyObjectsEnum::UILINE:
|
||||||
{
|
{
|
||||||
type = &PyUILineType;
|
type = &PyUILineType;
|
||||||
|
|
@ -152,6 +164,8 @@ static std::shared_ptr<UIDrawable> extractDrawable(PyObject* o) {
|
||||||
return ((PyUISpriteObject*)o)->data;
|
return ((PyUISpriteObject*)o)->data;
|
||||||
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridType))
|
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridType))
|
||||||
return ((PyUIGridObject*)o)->data;
|
return ((PyUIGridObject*)o)->data;
|
||||||
|
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridViewType))
|
||||||
|
return ((PyUIGridViewObject*)o)->data;
|
||||||
if (PyObject_IsInstance(o, (PyObject*)&PyUILineType))
|
if (PyObject_IsInstance(o, (PyObject*)&PyUILineType))
|
||||||
return ((PyUILineObject*)o)->data;
|
return ((PyUILineObject*)o)->data;
|
||||||
if (PyObject_IsInstance(o, (PyObject*)&PyUICircleType))
|
if (PyObject_IsInstance(o, (PyObject*)&PyUICircleType))
|
||||||
|
|
@ -1034,6 +1048,7 @@ PyObject* UICollection::repr(PyUICollectionObject* self)
|
||||||
case PyObjectsEnum::UICAPTION: caption_count++; break;
|
case PyObjectsEnum::UICAPTION: caption_count++; break;
|
||||||
case PyObjectsEnum::UISPRITE: sprite_count++; break;
|
case PyObjectsEnum::UISPRITE: sprite_count++; break;
|
||||||
case PyObjectsEnum::UIGRID: grid_count++; break;
|
case PyObjectsEnum::UIGRID: grid_count++; break;
|
||||||
|
case PyObjectsEnum::UIGRIDVIEW: other_count++; break;
|
||||||
default: other_count++; break;
|
default: other_count++; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "UICaption.h"
|
#include "UICaption.h"
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
#include "UILine.h"
|
#include "UILine.h"
|
||||||
#include "UICircle.h"
|
#include "UICircle.h"
|
||||||
#include "UIArc.h"
|
#include "UIArc.h"
|
||||||
|
|
@ -1032,7 +1033,7 @@ void UIDrawable::removeFromParent() {
|
||||||
}
|
}
|
||||||
frame->children_need_sort = true;
|
frame->children_need_sort = true;
|
||||||
}
|
}
|
||||||
else if (p->derived_type() == PyObjectsEnum::UIGRID) {
|
else if (p->derived_type() == PyObjectsEnum::UIGRID || p->derived_type() == PyObjectsEnum::UIGRIDVIEW) {
|
||||||
auto grid = std::static_pointer_cast<UIGrid>(p);
|
auto grid = std::static_pointer_cast<UIGrid>(p);
|
||||||
auto& children = *grid->children;
|
auto& children = *grid->children;
|
||||||
|
|
||||||
|
|
@ -1369,6 +1370,17 @@ PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
||||||
obj = (PyObject*)pyObj;
|
obj = (PyObject*)pyObj;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PyObjectsEnum::UIGRIDVIEW:
|
||||||
|
{
|
||||||
|
type = &mcrfpydef::PyUIGridViewType;
|
||||||
|
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||||
|
if (pyObj) {
|
||||||
|
pyObj->data = std::static_pointer_cast<UIGridView>(parent_ptr);
|
||||||
|
pyObj->weakreflist = NULL;
|
||||||
|
}
|
||||||
|
obj = (PyObject*)pyObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ enum PyObjectsEnum : int
|
||||||
UILINE,
|
UILINE,
|
||||||
UICIRCLE,
|
UICIRCLE,
|
||||||
UIARC,
|
UIARC,
|
||||||
UIVIEWPORT3D
|
UIVIEWPORT3D,
|
||||||
|
UIGRIDVIEW // #252: rendering view for GridData
|
||||||
};
|
};
|
||||||
|
|
||||||
class UIDrawable
|
class UIDrawable
|
||||||
|
|
|
||||||
516
src/UIGridView.cpp
Normal file
516
src/UIGridView.cpp
Normal file
|
|
@ -0,0 +1,516 @@
|
||||||
|
// UIGridView.cpp - Rendering view for GridData (#252)
|
||||||
|
#include "UIGridView.h"
|
||||||
|
#include "UIGrid.h"
|
||||||
|
#include "UIEntity.h"
|
||||||
|
#include "GameEngine.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
|
#include "Resources.h"
|
||||||
|
#include "Profiler.h"
|
||||||
|
#include "PyShader.h"
|
||||||
|
#include "PyUniformCollection.h"
|
||||||
|
#include "PyPositionHelper.h"
|
||||||
|
#include "PyVector.h"
|
||||||
|
#include "PythonObjectCache.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
UIGridView::UIGridView()
|
||||||
|
{
|
||||||
|
renderTexture.create(1, 1);
|
||||||
|
renderTextureSize = {1, 1};
|
||||||
|
output.setTextureRect(sf::IntRect(0, 0, 0, 0));
|
||||||
|
output.setPosition(0, 0);
|
||||||
|
output.setTexture(renderTexture.getTexture());
|
||||||
|
box.setSize(sf::Vector2f(256, 256));
|
||||||
|
box.setFillColor(sf::Color(0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
UIGridView::~UIGridView() {}
|
||||||
|
|
||||||
|
PyObjectsEnum UIGridView::derived_type()
|
||||||
|
{
|
||||||
|
return PyObjectsEnum::UIGRIDVIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridView::ensureRenderTextureSize()
|
||||||
|
{
|
||||||
|
sf::Vector2u resolution{1920, 1080};
|
||||||
|
if (Resources::game) {
|
||||||
|
resolution = Resources::game->getGameResolution();
|
||||||
|
}
|
||||||
|
unsigned int required_w = std::min(resolution.x, 4096u);
|
||||||
|
unsigned int required_h = std::min(resolution.y, 4096u);
|
||||||
|
|
||||||
|
if (renderTextureSize.x != required_w || renderTextureSize.y != required_h) {
|
||||||
|
renderTexture.create(required_w, required_h);
|
||||||
|
renderTextureSize = {required_w, required_h};
|
||||||
|
output.setTexture(renderTexture.getTexture());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::FloatRect UIGridView::get_bounds() const
|
||||||
|
{
|
||||||
|
return sf::FloatRect(box.getPosition(), box.getSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridView::move(float dx, float dy)
|
||||||
|
{
|
||||||
|
box.move(dx, dy);
|
||||||
|
position += sf::Vector2f(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridView::resize(float w, float h)
|
||||||
|
{
|
||||||
|
box.setSize(sf::Vector2f(w, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Vector2f UIGridView::getEffectiveCellSize() const
|
||||||
|
{
|
||||||
|
int cw = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int ch = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
return sf::Vector2f(cw * zoom, ch * zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<sf::Vector2i> UIGridView::screenToCell(sf::Vector2f screen_pos) const
|
||||||
|
{
|
||||||
|
if (!grid_data) return std::nullopt;
|
||||||
|
if (!box.getGlobalBounds().contains(screen_pos)) return std::nullopt;
|
||||||
|
|
||||||
|
int cw = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int ch = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
|
||||||
|
sf::Vector2f local = screen_pos - box.getPosition();
|
||||||
|
int left_sp = center_x - (box.getSize().x / 2.0 / zoom);
|
||||||
|
int top_sp = center_y - (box.getSize().y / 2.0 / zoom);
|
||||||
|
|
||||||
|
float gx = (local.x / zoom + left_sp) / cw;
|
||||||
|
float gy = (local.y / zoom + top_sp) / ch;
|
||||||
|
|
||||||
|
int cx = static_cast<int>(std::floor(gx));
|
||||||
|
int cy = static_cast<int>(std::floor(gy));
|
||||||
|
|
||||||
|
if (cx < 0 || cx >= grid_data->grid_w || cy < 0 || cy >= grid_data->grid_h)
|
||||||
|
return std::nullopt;
|
||||||
|
return sf::Vector2i(cx, cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridView::center_camera()
|
||||||
|
{
|
||||||
|
if (!grid_data) return;
|
||||||
|
int cw = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int ch = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
center_x = (grid_data->grid_w / 2.0f) * cw;
|
||||||
|
center_y = (grid_data->grid_h / 2.0f) * ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridView::center_camera(float tile_x, float tile_y)
|
||||||
|
{
|
||||||
|
int cw = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int ch = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
center_x = tile_x * cw;
|
||||||
|
center_y = tile_y * ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Render — adapted from UIGrid::render()
|
||||||
|
// =========================================================================
|
||||||
|
void UIGridView::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
{
|
||||||
|
if (!visible || !grid_data) return;
|
||||||
|
|
||||||
|
ensureRenderTextureSize();
|
||||||
|
|
||||||
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
|
||||||
|
bool has_camera_rotation = (camera_rotation != 0.0f);
|
||||||
|
float grid_w_px = box.getSize().x;
|
||||||
|
float grid_h_px = box.getSize().y;
|
||||||
|
|
||||||
|
float rad = camera_rotation * (M_PI / 180.0f);
|
||||||
|
float cos_r = std::cos(rad);
|
||||||
|
float sin_r = std::sin(rad);
|
||||||
|
float abs_cos = std::abs(cos_r);
|
||||||
|
float abs_sin = std::abs(sin_r);
|
||||||
|
float aabb_w = grid_w_px * abs_cos + grid_h_px * abs_sin;
|
||||||
|
float aabb_h = grid_w_px * abs_sin + grid_h_px * abs_cos;
|
||||||
|
|
||||||
|
sf::RenderTexture* activeTexture = &renderTexture;
|
||||||
|
|
||||||
|
if (has_camera_rotation) {
|
||||||
|
unsigned int needed_size = static_cast<unsigned int>(std::max(aabb_w, aabb_h) + 1);
|
||||||
|
if (rotationTextureSize < needed_size) {
|
||||||
|
rotationTexture.create(needed_size, needed_size);
|
||||||
|
rotationTextureSize = needed_size;
|
||||||
|
}
|
||||||
|
activeTexture = &rotationTexture;
|
||||||
|
activeTexture->clear(fill_color);
|
||||||
|
} else {
|
||||||
|
output.setPosition(box.getPosition() + offset);
|
||||||
|
output.setTextureRect(sf::IntRect(0, 0, grid_w_px, grid_h_px));
|
||||||
|
renderTexture.clear(fill_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
float render_w = has_camera_rotation ? aabb_w : grid_w_px;
|
||||||
|
float render_h = has_camera_rotation ? aabb_h : grid_h_px;
|
||||||
|
|
||||||
|
float center_x_sq = center_x / cell_width;
|
||||||
|
float center_y_sq = center_y / cell_height;
|
||||||
|
float width_sq = render_w / (cell_width * zoom);
|
||||||
|
float height_sq = render_h / (cell_height * zoom);
|
||||||
|
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||||
|
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||||
|
int left_spritepixels = center_x - (render_w / 2.0 / zoom);
|
||||||
|
int top_spritepixels = center_y - (render_h / 2.0 / zoom);
|
||||||
|
int x_limit = left_edge + width_sq + 2;
|
||||||
|
if (x_limit > grid_data->grid_w) x_limit = grid_data->grid_w;
|
||||||
|
int y_limit = top_edge + height_sq + 2;
|
||||||
|
if (y_limit > grid_data->grid_h) y_limit = grid_data->grid_h;
|
||||||
|
|
||||||
|
// Render layers below entities (z_index < 0)
|
||||||
|
grid_data->sortLayers();
|
||||||
|
for (auto& layer : grid_data->layers) {
|
||||||
|
if (layer->z_index >= 0) break;
|
||||||
|
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||||
|
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render entities
|
||||||
|
if (grid_data->entities) {
|
||||||
|
for (auto& e : *grid_data->entities) {
|
||||||
|
if (e->position.x < left_edge - 2 || e->position.x >= left_edge + width_sq + 2 ||
|
||||||
|
e->position.y < top_edge - 2 || e->position.y >= top_edge + height_sq + 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto& drawent = e->sprite;
|
||||||
|
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(e->position.x*cell_width - left_spritepixels + e->sprite_offset.x) * zoom,
|
||||||
|
(e->position.y*cell_height - top_spritepixels + e->sprite_offset.y) * zoom);
|
||||||
|
drawent.render(pixel_pos, *activeTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render layers above entities (z_index >= 0)
|
||||||
|
for (auto& layer : grid_data->layers) {
|
||||||
|
if (layer->z_index < 0) continue;
|
||||||
|
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||||
|
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children (grid-world pixel coordinates)
|
||||||
|
if (grid_data->children && !grid_data->children->empty()) {
|
||||||
|
if (grid_data->children_need_sort) {
|
||||||
|
std::sort(grid_data->children->begin(), grid_data->children->end(),
|
||||||
|
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
|
||||||
|
grid_data->children_need_sort = false;
|
||||||
|
}
|
||||||
|
for (auto& child : *grid_data->children) {
|
||||||
|
if (!child->visible) continue;
|
||||||
|
float child_grid_x = child->position.x / cell_width;
|
||||||
|
float child_grid_y = child->position.y / cell_height;
|
||||||
|
if (child_grid_x < left_edge - 2 || child_grid_x >= left_edge + width_sq + 2 ||
|
||||||
|
child_grid_y < top_edge - 2 || child_grid_y >= top_edge + height_sq + 2)
|
||||||
|
continue;
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(child->position.x - left_spritepixels) * zoom,
|
||||||
|
(child->position.y - top_spritepixels) * zoom);
|
||||||
|
child->render(pixel_pos, *activeTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perspective overlay
|
||||||
|
if (perspective_enabled) {
|
||||||
|
auto entity = perspective_entity.lock();
|
||||||
|
sf::RectangleShape overlay;
|
||||||
|
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
for (int x = std::max(0, (int)(left_edge - 1)); x < x_limit; x++) {
|
||||||
|
for (int y = std::max(0, (int)(top_edge - 1)); y < y_limit; y++) {
|
||||||
|
if (x < 0 || x >= grid_data->grid_w || y < 0 || y >= grid_data->grid_h) continue;
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(x*cell_width - left_spritepixels) * zoom,
|
||||||
|
(y*cell_height - top_spritepixels) * zoom);
|
||||||
|
int idx = y * grid_data->grid_w + x;
|
||||||
|
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
||||||
|
const auto& state = entity->gridstate[idx];
|
||||||
|
overlay.setPosition(pixel_pos);
|
||||||
|
if (!state.discovered) {
|
||||||
|
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||||
|
activeTexture->draw(overlay);
|
||||||
|
} else if (!state.visible) {
|
||||||
|
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||||
|
activeTexture->draw(overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int x = std::max(0, (int)(left_edge - 1)); x < x_limit; x++) {
|
||||||
|
for (int y = std::max(0, (int)(top_edge - 1)); y < y_limit; y++) {
|
||||||
|
if (x < 0 || x >= grid_data->grid_w || y < 0 || y >= grid_data->grid_h) continue;
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(x*cell_width - left_spritepixels) * zoom,
|
||||||
|
(y*cell_height - top_spritepixels) * zoom);
|
||||||
|
overlay.setPosition(pixel_pos);
|
||||||
|
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||||
|
activeTexture->draw(overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTexture->display();
|
||||||
|
|
||||||
|
// Camera rotation compositing
|
||||||
|
if (has_camera_rotation) {
|
||||||
|
renderTexture.clear(fill_color);
|
||||||
|
sf::Sprite rotatedSprite(rotationTexture.getTexture());
|
||||||
|
float tex_center_x = aabb_w / 2.0f;
|
||||||
|
float tex_center_y = aabb_h / 2.0f;
|
||||||
|
rotatedSprite.setOrigin(tex_center_x, tex_center_y);
|
||||||
|
rotatedSprite.setRotation(camera_rotation);
|
||||||
|
rotatedSprite.setPosition(grid_w_px / 2.0f, grid_h_px / 2.0f);
|
||||||
|
rotatedSprite.setTextureRect(sf::IntRect(0, 0, (int)aabb_w, (int)aabb_h));
|
||||||
|
renderTexture.draw(rotatedSprite);
|
||||||
|
renderTexture.display();
|
||||||
|
output.setPosition(box.getPosition() + offset);
|
||||||
|
output.setTextureRect(sf::IntRect(0, 0, grid_w_px, grid_h_px));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotation != 0.0f) {
|
||||||
|
output.setOrigin(origin);
|
||||||
|
output.setRotation(rotation);
|
||||||
|
output.setPosition(box.getPosition() + offset + origin);
|
||||||
|
} else {
|
||||||
|
output.setOrigin(0, 0);
|
||||||
|
output.setRotation(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shader && shader->shader) {
|
||||||
|
sf::Vector2f resolution(box.getSize().x, box.getSize().y);
|
||||||
|
PyShader::applyEngineUniforms(*shader->shader, resolution);
|
||||||
|
if (uniforms) uniforms->applyTo(*shader->shader);
|
||||||
|
target.draw(output, shader->shader.get());
|
||||||
|
} else {
|
||||||
|
target.draw(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UIDrawable* UIGridView::click_at(sf::Vector2f point)
|
||||||
|
{
|
||||||
|
if (!box.getGlobalBounds().contains(point)) return nullptr;
|
||||||
|
return this; // GridView consumes clicks within its bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property system
|
||||||
|
bool UIGridView::setProperty(const std::string& name, float value)
|
||||||
|
{
|
||||||
|
if (name == "center_x") { center_x = value; return true; }
|
||||||
|
if (name == "center_y") { center_y = value; return true; }
|
||||||
|
if (name == "zoom") { zoom = value; return true; }
|
||||||
|
if (name == "camera_rotation") { camera_rotation = value; return true; }
|
||||||
|
return UIDrawable::setProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UIGridView::setProperty(const std::string& name, const sf::Vector2f& value)
|
||||||
|
{
|
||||||
|
return UIDrawable::setProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UIGridView::getProperty(const std::string& name, float& value) const
|
||||||
|
{
|
||||||
|
if (name == "center_x") { value = center_x; return true; }
|
||||||
|
if (name == "center_y") { value = center_y; return true; }
|
||||||
|
if (name == "zoom") { value = zoom; return true; }
|
||||||
|
if (name == "camera_rotation") { value = camera_rotation; return true; }
|
||||||
|
return UIDrawable::getProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UIGridView::getProperty(const std::string& name, sf::Vector2f& value) const
|
||||||
|
{
|
||||||
|
return UIDrawable::getProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UIGridView::hasProperty(const std::string& name) const
|
||||||
|
{
|
||||||
|
if (name == "center_x" || name == "center_y" || name == "zoom" || name == "camera_rotation")
|
||||||
|
return true;
|
||||||
|
return UIDrawable::hasProperty(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Python API
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
|
||||||
|
PyObject* grid_obj = nullptr;
|
||||||
|
PyObject* pos_obj = nullptr;
|
||||||
|
PyObject* size_obj = nullptr;
|
||||||
|
float zoom_val = 1.0f;
|
||||||
|
PyObject* fill_obj = nullptr;
|
||||||
|
const char* name = nullptr;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOz", const_cast<char**>(kwlist),
|
||||||
|
&grid_obj, &pos_obj, &size_obj, &zoom_val, &fill_obj, &name)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->zoom = zoom_val;
|
||||||
|
if (name) self->data->UIDrawable::name = std::string(name);
|
||||||
|
|
||||||
|
// Parse grid
|
||||||
|
if (grid_obj && grid_obj != Py_None) {
|
||||||
|
if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||||
|
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
||||||
|
// Create aliasing shared_ptr: shares ownership with UIGrid, points to GridData base
|
||||||
|
self->data->grid_data = std::shared_ptr<GridData>(
|
||||||
|
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
|
||||||
|
self->data->ptex = pygrid->data->getTexture();
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pos
|
||||||
|
if (pos_obj && pos_obj != Py_None) {
|
||||||
|
sf::Vector2f pos = PyObject_to_sfVector2f(pos_obj);
|
||||||
|
if (PyErr_Occurred()) return -1;
|
||||||
|
self->data->position = pos;
|
||||||
|
self->data->box.setPosition(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse size
|
||||||
|
if (size_obj && size_obj != Py_None) {
|
||||||
|
sf::Vector2f size = PyObject_to_sfVector2f(size_obj);
|
||||||
|
if (PyErr_Occurred()) return -1;
|
||||||
|
self->data->box.setSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse fill_color
|
||||||
|
if (fill_obj && fill_obj != Py_None) {
|
||||||
|
self->data->fill_color = PyColor::fromPy(fill_obj);
|
||||||
|
if (PyErr_Occurred()) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center camera on grid if we have one
|
||||||
|
if (self->data->grid_data) {
|
||||||
|
self->data->center_camera();
|
||||||
|
self->data->ensureRenderTextureSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
self->weakreflist = NULL;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridView::repr(PyUIGridViewObject* self)
|
||||||
|
{
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << "<GridView";
|
||||||
|
if (self->data->grid_data) {
|
||||||
|
ss << " grid=(" << self->data->grid_data->grid_w << "x" << self->data->grid_data->grid_h << ")";
|
||||||
|
} else {
|
||||||
|
ss << " grid=None";
|
||||||
|
}
|
||||||
|
ss << " pos=(" << self->data->box.getPosition().x << ", " << self->data->box.getPosition().y << ")"
|
||||||
|
<< " size=(" << self->data->box.getSize().x << ", " << self->data->box.getSize().y << ")"
|
||||||
|
<< " zoom=" << self->data->zoom << ">";
|
||||||
|
return PyUnicode_FromString(ss.str().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property getters/setters
|
||||||
|
PyObject* UIGridView::get_grid(PyUIGridViewObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (!self->data->grid_data) Py_RETURN_NONE;
|
||||||
|
// TODO: return the Grid wrapper for grid_data
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridView::set_grid(PyUIGridViewObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (value == Py_None) {
|
||||||
|
self->data->grid_data = nullptr;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
PyUIGridObject* pygrid = (PyUIGridObject*)value;
|
||||||
|
self->data->grid_data = std::shared_ptr<GridData>(
|
||||||
|
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
|
||||||
|
self->data->ptex = pygrid->data->getTexture();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridView::get_center(PyUIGridViewObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return sfVector2f_to_PyObject(sf::Vector2f(self->data->center_x, self->data->center_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridView::set_center(PyUIGridViewObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
sf::Vector2f vec = PyObject_to_sfVector2f(value);
|
||||||
|
if (PyErr_Occurred()) return -1;
|
||||||
|
self->data->center_x = vec.x;
|
||||||
|
self->data->center_y = vec.y;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridView::get_zoom(PyUIGridViewObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyFloat_FromDouble(self->data->zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridView::set_zoom(PyUIGridViewObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
double val = PyFloat_AsDouble(value);
|
||||||
|
if (val == -1.0 && PyErr_Occurred()) return -1;
|
||||||
|
self->data->zoom = static_cast<float>(val);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridView::get_fill_color(PyUIGridViewObject* self, void* closure)
|
||||||
|
{
|
||||||
|
auto type = &mcrfpydef::PyColorType;
|
||||||
|
auto obj = (PyColorObject*)type->tp_alloc(type, 0);
|
||||||
|
if (obj) obj->data = self->data->fill_color;
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridView::set_fill_color(PyUIGridViewObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
self->data->fill_color = PyColor::fromPy(value);
|
||||||
|
if (PyErr_Occurred()) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (!self->data->ptex) Py_RETURN_NONE;
|
||||||
|
// TODO: return texture wrapper
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods and getsetters arrays
|
||||||
|
PyMethodDef UIGridView_all_methods[] = {
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
PyGetSetDef UIGridView::getsetters[] = {
|
||||||
|
{"grid", (getter)UIGridView::get_grid, (setter)UIGridView::set_grid,
|
||||||
|
"The Grid data this view renders.", NULL},
|
||||||
|
{"center", (getter)UIGridView::get_center, (setter)UIGridView::set_center,
|
||||||
|
"Camera center point in pixel coordinates.", NULL},
|
||||||
|
{"zoom", (getter)UIGridView::get_zoom, (setter)UIGridView::set_zoom,
|
||||||
|
"Zoom level for rendering.", NULL},
|
||||||
|
{"fill_color", (getter)UIGridView::get_fill_color, (setter)UIGridView::set_fill_color,
|
||||||
|
"Background fill color.", NULL},
|
||||||
|
{"texture", (getter)UIGridView::get_texture, NULL,
|
||||||
|
"Texture used for tile rendering (read-only).", NULL},
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
158
src/UIGridView.h
Normal file
158
src/UIGridView.h
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
#pragma once
|
||||||
|
// UIGridView.h - Rendering view for GridData (#252)
|
||||||
|
//
|
||||||
|
// GridView is a UIDrawable that renders a GridData object. Multiple GridViews
|
||||||
|
// can reference the same GridData for split-screen, minimap, etc.
|
||||||
|
// GridView holds rendering state (camera, zoom, perspective) independently.
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include "structmember.h"
|
||||||
|
#include "UIDrawable.h"
|
||||||
|
#include "UIBase.h"
|
||||||
|
#include "GridData.h"
|
||||||
|
#include "PyTexture.h"
|
||||||
|
#include "PyColor.h"
|
||||||
|
#include "PyVector.h"
|
||||||
|
#include "PyCallable.h"
|
||||||
|
#include "PyDrawable.h"
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class UIGrid;
|
||||||
|
class UIGridView;
|
||||||
|
|
||||||
|
// Python object struct (UIGridView forward-declared above)
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::shared_ptr<UIGridView> data;
|
||||||
|
PyObject* weakreflist;
|
||||||
|
} PyUIGridViewObject;
|
||||||
|
|
||||||
|
class UIGridView : public UIDrawable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
UIGridView();
|
||||||
|
~UIGridView();
|
||||||
|
|
||||||
|
void render(sf::Vector2f offset, sf::RenderTarget& target) override final;
|
||||||
|
PyObjectsEnum derived_type() override final;
|
||||||
|
UIDrawable* click_at(sf::Vector2f point) override final;
|
||||||
|
|
||||||
|
sf::FloatRect get_bounds() const override;
|
||||||
|
void move(float dx, float dy) override;
|
||||||
|
void resize(float w, float h) override;
|
||||||
|
|
||||||
|
// The grid data this view renders
|
||||||
|
std::shared_ptr<GridData> grid_data;
|
||||||
|
|
||||||
|
// Rendering state (independent per view)
|
||||||
|
std::shared_ptr<PyTexture> ptex;
|
||||||
|
sf::RectangleShape box;
|
||||||
|
float center_x = 0, center_y = 0, zoom = 1.0f;
|
||||||
|
float camera_rotation = 0.0f;
|
||||||
|
sf::Color fill_color{8, 8, 8, 255};
|
||||||
|
|
||||||
|
// Perspective (per-view)
|
||||||
|
std::weak_ptr<UIEntity> perspective_entity;
|
||||||
|
bool perspective_enabled = false;
|
||||||
|
|
||||||
|
// Render textures
|
||||||
|
sf::Sprite sprite_proto, output;
|
||||||
|
sf::RenderTexture renderTexture;
|
||||||
|
sf::Vector2u renderTextureSize{0, 0};
|
||||||
|
void ensureRenderTextureSize();
|
||||||
|
sf::RenderTexture rotationTexture;
|
||||||
|
unsigned int rotationTextureSize = 0;
|
||||||
|
|
||||||
|
// Property system for animations
|
||||||
|
bool setProperty(const std::string& name, float value) override;
|
||||||
|
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||||
|
bool getProperty(const std::string& name, float& value) const override;
|
||||||
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
|
// Camera positioning
|
||||||
|
void center_camera();
|
||||||
|
void center_camera(float tile_x, float tile_y);
|
||||||
|
|
||||||
|
// Cell coordinate conversion
|
||||||
|
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
|
||||||
|
sf::Vector2f getEffectiveCellSize() const;
|
||||||
|
|
||||||
|
static constexpr int DEFAULT_CELL_WIDTH = 16;
|
||||||
|
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Python API
|
||||||
|
// =========================================================================
|
||||||
|
static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* repr(PyUIGridViewObject* self);
|
||||||
|
|
||||||
|
static PyObject* get_grid(PyUIGridViewObject* self, void* closure);
|
||||||
|
static int set_grid(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_center(PyUIGridViewObject* self, void* closure);
|
||||||
|
static int set_center(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_zoom(PyUIGridViewObject* self, void* closure);
|
||||||
|
static int set_zoom(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_fill_color(PyUIGridViewObject* self, void* closure);
|
||||||
|
static int set_fill_color(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_texture(PyUIGridViewObject* self, void* closure);
|
||||||
|
|
||||||
|
static PyMethodDef methods[];
|
||||||
|
static PyGetSetDef getsetters[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward declaration of methods array
|
||||||
|
extern PyMethodDef UIGridView_all_methods[];
|
||||||
|
|
||||||
|
namespace mcrfpydef {
|
||||||
|
inline PyTypeObject PyUIGridViewType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.GridView",
|
||||||
|
.tp_basicsize = sizeof(PyUIGridViewObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)[](PyObject* self)
|
||||||
|
{
|
||||||
|
PyUIGridViewObject* obj = (PyUIGridViewObject*)self;
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
|
if (obj->weakreflist != NULL) {
|
||||||
|
PyObject_ClearWeakRefs(self);
|
||||||
|
}
|
||||||
|
obj->data.reset();
|
||||||
|
Py_TYPE(self)->tp_free(self);
|
||||||
|
},
|
||||||
|
.tp_repr = (reprfunc)UIGridView::repr,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"GridView(grid=None, pos=None, size=None, **kwargs)\n\n"
|
||||||
|
"A rendering view for a Grid's data. Multiple GridViews can display\n"
|
||||||
|
"the same Grid with different camera positions, zoom levels, etc.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" grid (Grid): The Grid to render. Required.\n"
|
||||||
|
" pos (tuple): Position as (x, y). Default: (0, 0)\n"
|
||||||
|
" size (tuple): Size as (w, h). Default: (256, 256)\n\n"
|
||||||
|
"Keyword Args:\n"
|
||||||
|
" zoom (float): Zoom level. Default: 1.0\n"
|
||||||
|
" center (tuple): Camera center (x, y) in pixels. Default: grid center\n"
|
||||||
|
" fill_color (Color): Background color. Default: dark gray\n"),
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_methods = UIGridView_all_methods,
|
||||||
|
.tp_getset = UIGridView::getsetters,
|
||||||
|
.tp_base = &mcrfpydef::PyDrawableType,
|
||||||
|
.tp_init = (initproc)UIGridView::init,
|
||||||
|
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||||
|
{
|
||||||
|
PyUIGridViewObject* self = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||||
|
if (self) {
|
||||||
|
self->data = std::make_shared<UIGridView>();
|
||||||
|
self->weakreflist = nullptr;
|
||||||
|
}
|
||||||
|
return (PyObject*)self;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
91
tests/unit/gridview_test.py
Normal file
91
tests/unit/gridview_test.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""Unit test for GridView (#252): rendering view for Grid data."""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_gridview_creation():
|
||||||
|
"""GridView can be created with a Grid reference."""
|
||||||
|
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||||
|
view = mcrfpy.GridView(grid=grid, pos=(200, 0), size=(160, 160))
|
||||||
|
assert view.zoom == 1.0
|
||||||
|
print("PASS: GridView creation")
|
||||||
|
|
||||||
|
def test_gridview_properties():
|
||||||
|
"""GridView properties can be read and set."""
|
||||||
|
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||||
|
view = mcrfpy.GridView(grid=grid, pos=(0, 0), size=(200, 200))
|
||||||
|
|
||||||
|
view.zoom = 2.0
|
||||||
|
assert abs(view.zoom - 2.0) < 0.01
|
||||||
|
|
||||||
|
view.center = (50, 50)
|
||||||
|
c = view.center
|
||||||
|
assert abs(c.x - 50) < 0.01 and abs(c.y - 50) < 0.01
|
||||||
|
|
||||||
|
view.fill_color = mcrfpy.Color(255, 0, 0)
|
||||||
|
assert view.fill_color.r == 255
|
||||||
|
print("PASS: GridView properties")
|
||||||
|
|
||||||
|
def test_gridview_in_scene():
|
||||||
|
"""GridView can be added to a scene's children."""
|
||||||
|
scene = mcrfpy.Scene("test_gv_scene")
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
|
||||||
|
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||||
|
view = mcrfpy.GridView(grid=grid, pos=(200, 0), size=(160, 160))
|
||||||
|
|
||||||
|
scene.children.append(grid)
|
||||||
|
scene.children.append(view)
|
||||||
|
assert len(scene.children) == 2
|
||||||
|
print("PASS: GridView in scene")
|
||||||
|
|
||||||
|
def test_gridview_multi_view():
|
||||||
|
"""Multiple GridViews can reference the same Grid."""
|
||||||
|
scene = mcrfpy.Scene("test_gv_multi")
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
|
||||||
|
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(20, 20), texture=tex, pos=(0, 0), size=(320, 320))
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
view1 = mcrfpy.GridView(grid=grid, pos=(350, 0), size=(160, 160), zoom=0.5)
|
||||||
|
view2 = mcrfpy.GridView(grid=grid, pos=(350, 170), size=(160, 160), zoom=2.0)
|
||||||
|
|
||||||
|
scene.children.append(view1)
|
||||||
|
scene.children.append(view2)
|
||||||
|
|
||||||
|
# Both views exist with different zoom
|
||||||
|
assert abs(view1.zoom - 0.5) < 0.01
|
||||||
|
assert abs(view2.zoom - 2.0) < 0.01
|
||||||
|
assert len(scene.children) == 3
|
||||||
|
print("PASS: Multiple GridViews of same Grid")
|
||||||
|
|
||||||
|
def test_gridview_repr():
|
||||||
|
"""GridView has useful repr."""
|
||||||
|
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(15, 10), texture=tex, pos=(0, 0), size=(240, 160))
|
||||||
|
view = mcrfpy.GridView(grid=grid, pos=(0, 0), size=(240, 160))
|
||||||
|
r = repr(view)
|
||||||
|
assert "GridView" in r
|
||||||
|
assert "15x10" in r
|
||||||
|
print("PASS: GridView repr")
|
||||||
|
|
||||||
|
def test_gridview_no_grid():
|
||||||
|
"""GridView without a grid doesn't crash."""
|
||||||
|
view = mcrfpy.GridView()
|
||||||
|
assert view.grid is None
|
||||||
|
r = repr(view)
|
||||||
|
assert "None" in r
|
||||||
|
print("PASS: GridView without grid")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_gridview_creation()
|
||||||
|
test_gridview_properties()
|
||||||
|
test_gridview_in_scene()
|
||||||
|
test_gridview_multi_view()
|
||||||
|
test_gridview_repr()
|
||||||
|
test_gridview_no_grid()
|
||||||
|
print("All GridView tests passed")
|
||||||
|
sys.exit(0)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue