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 "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
#include "UIArc.h"
|
||||
|
|
@ -557,6 +558,17 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
obj = (PyObject*)pyObj;
|
||||
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:
|
||||
{
|
||||
type = &mcrfpydef::PyUILineType;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UIEntity.h"
|
||||
#include "ImGuiConsole.h"
|
||||
#include "PythonObjectCache.h"
|
||||
|
|
@ -285,6 +286,7 @@ const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) {
|
|||
case PyObjectsEnum::UICAPTION: return "Caption";
|
||||
case PyObjectsEnum::UISPRITE: return "Sprite";
|
||||
case PyObjectsEnum::UIGRID: return "Grid";
|
||||
case PyObjectsEnum::UIGRIDVIEW: return "GridView";
|
||||
case PyObjectsEnum::UILINE: return "Line";
|
||||
case PyObjectsEnum::UICIRCLE: return "Circle";
|
||||
case PyObjectsEnum::UIARC: return "Arc";
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include "PyInputState.h"
|
||||
#include "PyBehavior.h"
|
||||
#include "PyTrigger.h"
|
||||
#include "UIGridView.h"
|
||||
#include "PySound.h"
|
||||
#include "PySoundBuffer.h"
|
||||
#include "PyMusic.h"
|
||||
|
|
@ -465,6 +466,7 @@ PyObject* PyInit_mcrfpy()
|
|||
PyTypeObject* ui_types_with_callbacks[] = {
|
||||
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyUIGridType,
|
||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||
&mcrfpydef::PyUIGridViewType,
|
||||
nullptr
|
||||
};
|
||||
for (int i = 0; ui_types_with_callbacks[i] != nullptr; i++) {
|
||||
|
|
@ -482,6 +484,7 @@ PyObject* PyInit_mcrfpy()
|
|||
/*UI widgets*/
|
||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||
&mcrfpydef::PyUIGridViewType,
|
||||
|
||||
/*3D entities*/
|
||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||
|
|
@ -635,6 +638,7 @@ PyObject* PyInit_mcrfpy()
|
|||
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
|
||||
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
|
||||
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
|
||||
mcrfpydef::PyUIGridViewType.tp_weaklistoffset = offsetof(PyUIGridViewObject, weakreflist);
|
||||
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
|
||||
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
||||
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
||||
|
|
@ -1596,6 +1600,16 @@ static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collect
|
|||
}
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "PyCallable.h"
|
||||
#include "UIFrame.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridView.h"
|
||||
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
||||
#include "PythonObjectCache.h" // #184 - For subclass callback support
|
||||
#include "McRFPy_API.h" // For Vector type access
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
#include "UIArc.h"
|
||||
|
|
@ -78,6 +79,17 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
obj = (PyObject*)pyObj;
|
||||
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:
|
||||
{
|
||||
type = &PyUILineType;
|
||||
|
|
@ -152,6 +164,8 @@ static std::shared_ptr<UIDrawable> extractDrawable(PyObject* o) {
|
|||
return ((PyUISpriteObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridType))
|
||||
return ((PyUIGridObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridViewType))
|
||||
return ((PyUIGridViewObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUILineType))
|
||||
return ((PyUILineObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUICircleType))
|
||||
|
|
@ -1034,6 +1048,7 @@ PyObject* UICollection::repr(PyUICollectionObject* self)
|
|||
case PyObjectsEnum::UICAPTION: caption_count++; break;
|
||||
case PyObjectsEnum::UISPRITE: sprite_count++; break;
|
||||
case PyObjectsEnum::UIGRID: grid_count++; break;
|
||||
case PyObjectsEnum::UIGRIDVIEW: other_count++; break;
|
||||
default: other_count++; break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
#include "UIArc.h"
|
||||
|
|
@ -1032,7 +1033,7 @@ void UIDrawable::removeFromParent() {
|
|||
}
|
||||
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& children = *grid->children;
|
||||
|
||||
|
|
@ -1369,6 +1370,17 @@ PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
|||
obj = (PyObject*)pyObj;
|
||||
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:
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ enum PyObjectsEnum : int
|
|||
UILINE,
|
||||
UICIRCLE,
|
||||
UIARC,
|
||||
UIVIEWPORT3D
|
||||
UIVIEWPORT3D,
|
||||
UIGRIDVIEW // #252: rendering view for GridData
|
||||
};
|
||||
|
||||
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