Three things, sorry. SDL composite texture bugfix, sprite offset position, some Grid render efficiencies
This commit is contained in:
parent
456e5e676e
commit
120b0aa2a4
5 changed files with 273 additions and 27 deletions
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
|
|
||||||
UIEntity::UIEntity()
|
UIEntity::UIEntity()
|
||||||
: self(nullptr), grid(nullptr), position(0.0f, 0.0f)
|
: self(nullptr), grid(nullptr), position(0.0f, 0.0f), sprite_offset(0.0f, 0.0f)
|
||||||
{
|
{
|
||||||
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
|
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
|
||||||
// gridstate vector starts empty - will be lazily initialized when needed
|
// gridstate vector starts empty - will be lazily initialized when needed
|
||||||
|
|
@ -172,19 +172,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
float opacity = 1.0f;
|
float opacity = 1.0f;
|
||||||
const char* name = nullptr;
|
const char* name = nullptr;
|
||||||
float x = 0.0f, y = 0.0f;
|
float x = 0.0f, y = 0.0f;
|
||||||
|
PyObject* sprite_offset_obj = nullptr;
|
||||||
|
|
||||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
|
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||||
// Keyword-only args
|
// Keyword-only args
|
||||||
"grid", "visible", "opacity", "name", "x", "y",
|
"grid", "visible", "opacity", "name", "x", "y", "sprite_offset",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse arguments with | for optional positional args
|
// Parse arguments with | for optional positional args
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzffO", const_cast<char**>(kwlist),
|
||||||
&grid_pos_obj, &texture, &sprite_index, // Positional
|
&grid_pos_obj, &texture, &sprite_index, // Positional
|
||||||
&grid_obj, &visible, &opacity, &name, &x, &y)) {
|
&grid_obj, &visible, &opacity, &name, &x, &y, &sprite_offset_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,6 +259,15 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
// Set position using grid coordinates
|
// Set position using grid coordinates
|
||||||
self->data->position = sf::Vector2f(x, y);
|
self->data->position = sf::Vector2f(x, y);
|
||||||
|
|
||||||
|
// Handle sprite_offset argument (optional tuple, default (0,0))
|
||||||
|
if (sprite_offset_obj && sprite_offset_obj != Py_None) {
|
||||||
|
sf::Vector2f offset = PyObject_to_sfVector2f(sprite_offset_obj);
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->sprite_offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
// Set other properties (delegate to sprite)
|
// Set other properties (delegate to sprite)
|
||||||
self->data->sprite.visible = visible;
|
self->data->sprite.visible = visible;
|
||||||
self->data->sprite.opacity = opacity;
|
self->data->sprite.opacity = opacity;
|
||||||
|
|
@ -708,6 +718,47 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sprite_offset property - Vector (tuple)
|
||||||
|
PyObject* UIEntity::get_sprite_offset(PyUIEntityObject* self, void* closure) {
|
||||||
|
return sfVector2f_to_PyObject(self->data->sprite_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIEntity::set_sprite_offset(PyUIEntityObject* self, PyObject* value, void* closure) {
|
||||||
|
sf::Vector2f vec = PyObject_to_sfVector2f(value);
|
||||||
|
if (PyErr_Occurred()) return -1;
|
||||||
|
self->data->sprite_offset = vec;
|
||||||
|
if (self->data->grid) self->data->grid->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sprite_offset_x / sprite_offset_y individual components
|
||||||
|
PyObject* UIEntity::get_sprite_offset_member(PyUIEntityObject* self, void* closure) {
|
||||||
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
||||||
|
if (member_ptr == 0)
|
||||||
|
return PyFloat_FromDouble(self->data->sprite_offset.x);
|
||||||
|
else
|
||||||
|
return PyFloat_FromDouble(self->data->sprite_offset.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIEntity::set_sprite_offset_member(PyUIEntityObject* self, PyObject* value, void* closure) {
|
||||||
|
float val;
|
||||||
|
if (PyFloat_Check(value))
|
||||||
|
val = PyFloat_AsDouble(value);
|
||||||
|
else if (PyLong_Check(value))
|
||||||
|
val = PyLong_AsLong(value);
|
||||||
|
else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "sprite_offset component must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
||||||
|
if (member_ptr == 0)
|
||||||
|
self->data->sprite_offset.x = val;
|
||||||
|
else
|
||||||
|
self->data->sprite_offset.y = val;
|
||||||
|
if (self->data->grid) self->data->grid->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
||||||
{
|
{
|
||||||
// Check if entity has a grid
|
// Check if entity has a grid
|
||||||
|
|
@ -1046,6 +1097,12 @@ PyGetSetDef UIEntity::getsetters[] = {
|
||||||
"Collection of shader uniforms (read-only access to collection). "
|
"Collection of shader uniforms (read-only access to collection). "
|
||||||
"Set uniforms via dict-like syntax: entity.uniforms['name'] = value. "
|
"Set uniforms via dict-like syntax: entity.uniforms['name'] = value. "
|
||||||
"Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.", NULL},
|
"Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.", NULL},
|
||||||
|
{"sprite_offset", (getter)UIEntity::get_sprite_offset, (setter)UIEntity::set_sprite_offset,
|
||||||
|
"Pixel offset for oversized sprites (Vector). Applied pre-zoom during grid rendering.", NULL},
|
||||||
|
{"sprite_offset_x", (getter)UIEntity::get_sprite_offset_member, (setter)UIEntity::set_sprite_offset_member,
|
||||||
|
"X component of sprite pixel offset.", (void*)0},
|
||||||
|
{"sprite_offset_y", (getter)UIEntity::get_sprite_offset_member, (setter)UIEntity::set_sprite_offset_member,
|
||||||
|
"Y component of sprite pixel offset.", (void*)1},
|
||||||
{NULL} /* Sentinel */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1070,18 +1127,28 @@ bool UIEntity::setProperty(const std::string& name, float value) {
|
||||||
if (name == "draw_x" || name == "x") { // #176 - draw_x is preferred, x is alias
|
if (name == "draw_x" || name == "x") { // #176 - draw_x is preferred, x is alias
|
||||||
position.x = value;
|
position.x = value;
|
||||||
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
||||||
if (grid) grid->markDirty(); // #144 - Propagate to parent grid for texture caching
|
if (grid) grid->markCompositeDirty(); // #144 - Propagate to parent grid for texture caching
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (name == "draw_y" || name == "y") { // #176 - draw_y is preferred, y is alias
|
else if (name == "draw_y" || name == "y") { // #176 - draw_y is preferred, y is alias
|
||||||
position.y = value;
|
position.y = value;
|
||||||
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
||||||
if (grid) grid->markDirty(); // #144 - Propagate to parent grid for texture caching
|
if (grid) grid->markCompositeDirty(); // #144 - Propagate to parent grid for texture caching
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (name == "sprite_scale") {
|
else if (name == "sprite_scale") {
|
||||||
sprite.setScale(sf::Vector2f(value, value));
|
sprite.setScale(sf::Vector2f(value, value));
|
||||||
if (grid) grid->markDirty(); // #144 - Content change
|
if (grid) grid->markCompositeDirty(); // #144 - Content change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (name == "sprite_offset_x") {
|
||||||
|
sprite_offset.x = value;
|
||||||
|
if (grid) grid->markCompositeDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (name == "sprite_offset_y") {
|
||||||
|
sprite_offset.y = value;
|
||||||
|
if (grid) grid->markCompositeDirty();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// #106: Shader uniform properties - delegate to sprite
|
// #106: Shader uniform properties - delegate to sprite
|
||||||
|
|
@ -1113,6 +1180,14 @@ bool UIEntity::getProperty(const std::string& name, float& value) const {
|
||||||
value = sprite.getScale().x; // Assuming uniform scale
|
value = sprite.getScale().x; // Assuming uniform scale
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "sprite_offset_x") {
|
||||||
|
value = sprite_offset.x;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (name == "sprite_offset_y") {
|
||||||
|
value = sprite_offset.y;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// #106: Shader uniform properties - delegate to sprite
|
// #106: Shader uniform properties - delegate to sprite
|
||||||
if (sprite.getShaderProperty(name, value)) {
|
if (sprite.getShaderProperty(name, value)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1122,7 +1197,8 @@ bool UIEntity::getProperty(const std::string& name, float& value) const {
|
||||||
|
|
||||||
bool UIEntity::hasProperty(const std::string& name) const {
|
bool UIEntity::hasProperty(const std::string& name) const {
|
||||||
// #176 - Float properties (draw_x/draw_y preferred, x/y are aliases)
|
// #176 - Float properties (draw_x/draw_y preferred, x/y are aliases)
|
||||||
if (name == "draw_x" || name == "draw_y" || name == "x" || name == "y" || name == "sprite_scale") {
|
if (name == "draw_x" || name == "draw_y" || name == "x" || name == "y" || name == "sprite_scale"
|
||||||
|
|| name == "sprite_offset_x" || name == "sprite_offset_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Int properties
|
// Int properties
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ public:
|
||||||
std::vector<UIGridPointState> gridstate;
|
std::vector<UIGridPointState> gridstate;
|
||||||
UISprite sprite;
|
UISprite sprite;
|
||||||
sf::Vector2f position; //(x,y) in grid coordinates; float for animation
|
sf::Vector2f position; //(x,y) in grid coordinates; float for animation
|
||||||
|
sf::Vector2f sprite_offset; // pixel offset for oversized sprites (applied pre-zoom)
|
||||||
//void render(sf::Vector2f); //override final;
|
//void render(sf::Vector2f); //override final;
|
||||||
|
|
||||||
UIEntity();
|
UIEntity();
|
||||||
|
|
@ -115,6 +116,10 @@ public:
|
||||||
static int set_grid_int_member(PyUIEntityObject* self, PyObject* value, void* closure);
|
static int set_grid_int_member(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_grid(PyUIEntityObject* self, void* closure);
|
static PyObject* get_grid(PyUIEntityObject* self, void* closure);
|
||||||
static int set_grid(PyUIEntityObject* self, PyObject* value, void* closure);
|
static int set_grid(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_sprite_offset(PyUIEntityObject* self, void* closure);
|
||||||
|
static int set_sprite_offset(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_sprite_offset_member(PyUIEntityObject* self, void* closure);
|
||||||
|
static int set_sprite_offset_member(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
static PyObject* repr(PyUIEntityObject* self);
|
static PyObject* repr(PyUIEntityObject* self);
|
||||||
|
|
@ -143,7 +148,8 @@ namespace mcrfpydef {
|
||||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||||
" name (str): Element name for finding. Default: None\n"
|
" name (str): Element name for finding. Default: None\n"
|
||||||
" x (float): X grid position override (tile coords). Default: 0\n"
|
" x (float): X grid position override (tile coords). Default: 0\n"
|
||||||
" y (float): Y grid position override (tile coords). Default: 0\n\n"
|
" y (float): Y grid position override (tile coords). Default: 0\n"
|
||||||
|
" sprite_offset (tuple): Pixel offset for oversized sprites. Default: (0, 0)\n\n"
|
||||||
"Attributes:\n"
|
"Attributes:\n"
|
||||||
" pos (Vector): Pixel position relative to grid (requires grid attachment)\n"
|
" pos (Vector): Pixel position relative to grid (requires grid attachment)\n"
|
||||||
" x, y (float): Pixel position components (requires grid attachment)\n"
|
" x, y (float): Pixel position components (requires grid attachment)\n"
|
||||||
|
|
@ -154,7 +160,10 @@ namespace mcrfpydef {
|
||||||
" sprite_index (int): Current sprite index\n"
|
" sprite_index (int): Current sprite index\n"
|
||||||
" visible (bool): Visibility state\n"
|
" visible (bool): Visibility state\n"
|
||||||
" opacity (float): Opacity value\n"
|
" opacity (float): Opacity value\n"
|
||||||
" name (str): Element name"),
|
" name (str): Element name\n"
|
||||||
|
" sprite_offset (Vector): Pixel offset for oversized sprites\n"
|
||||||
|
" sprite_offset_x (float): X component of sprite offset\n"
|
||||||
|
" sprite_offset_y (float): Y component of sprite offset"),
|
||||||
.tp_methods = UIEntity_all_methods,
|
.tp_methods = UIEntity_all_methods,
|
||||||
.tp_getset = UIEntity::getsetters,
|
.tp_getset = UIEntity::getsetters,
|
||||||
.tp_base = NULL,
|
.tp_base = NULL,
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,26 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// #228 - Ensure renderTexture matches current game resolution
|
// #228 - Ensure renderTexture matches current game resolution
|
||||||
ensureRenderTextureSize();
|
ensureRenderTextureSize();
|
||||||
|
|
||||||
|
if (!render_dirty && !composite_dirty) {
|
||||||
|
if (shader && shader->shader) {
|
||||||
|
sf::Vector2f resolution(box.getSize().x, box.getSize().y);
|
||||||
|
PyShader::applyEngineUniforms(*shader->shader, resolution);
|
||||||
|
|
||||||
|
// Apply user uniforms
|
||||||
|
if (uniforms) {
|
||||||
|
uniforms->applyTo(*shader->shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.draw(output, shader->shader.get());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.setPosition(box.getPosition() + offset);
|
||||||
|
target.draw(output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Apply opacity to output sprite
|
// TODO: Apply opacity to output sprite
|
||||||
|
|
||||||
// Get cell dimensions - use texture if available, otherwise defaults
|
// Get cell dimensions - use texture if available, otherwise defaults
|
||||||
|
|
@ -228,9 +248,9 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
|
||||||
for (auto e : *entities) {
|
for (auto e : *entities) {
|
||||||
// Skip out-of-bounds entities for performance
|
// Skip out-of-bounds entities for performance
|
||||||
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
// Check if entity is within visible bounds (with 2 cell margin for offset/oversized sprites)
|
||||||
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
|
if (e->position.x < left_edge - 2 || e->position.x >= left_edge + width_sq + 2 ||
|
||||||
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
|
e->position.y < top_edge - 2 || e->position.y >= top_edge + height_sq + 2) {
|
||||||
continue; // Skip this entity as it's not visible
|
continue; // Skip this entity as it's not visible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,8 +259,8 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
//drawent.setScale(zoom, zoom);
|
//drawent.setScale(zoom, zoom);
|
||||||
drawent.setScale(sf::Vector2f(zoom, zoom));
|
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||||
auto pixel_pos = sf::Vector2f(
|
auto pixel_pos = sf::Vector2f(
|
||||||
(e->position.x*cell_width - left_spritepixels) * zoom,
|
(e->position.x*cell_width - left_spritepixels + e->sprite_offset.x) * zoom,
|
||||||
(e->position.y*cell_height - top_spritepixels) * zoom );
|
(e->position.y*cell_height - top_spritepixels + e->sprite_offset.y) * zoom );
|
||||||
drawent.render(pixel_pos, *activeTexture);
|
drawent.render(pixel_pos, *activeTexture);
|
||||||
|
|
||||||
entitiesRendered++;
|
entitiesRendered++;
|
||||||
|
|
|
||||||
|
|
@ -1111,11 +1111,16 @@ Texture::~Texture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture::Texture(const Texture& other)
|
Texture::Texture(const Texture& other)
|
||||||
: size_(other.size_), smooth_(other.smooth_), repeated_(other.repeated_) {
|
: size_(other.size_), smooth_(other.smooth_), repeated_(other.repeated_),
|
||||||
if (other.textureId_) {
|
flippedY_(false) {
|
||||||
// Create new texture with same properties
|
if (other.textureId_ && size_.x > 0 && size_.y > 0) {
|
||||||
textureId_ = SDL2Renderer::getInstance().createTexture(size_.x, size_.y, nullptr);
|
// Read back pixel data from source texture via FBO, then upload to new texture.
|
||||||
// Note: Would need to copy pixel data for full implementation
|
// copyToImage() normalizes orientation (flips FBO textures to top-to-bottom),
|
||||||
|
// so the copy is always in standard orientation: flippedY_ = false.
|
||||||
|
Image img = other.copyToImage();
|
||||||
|
textureId_ = SDL2Renderer::getInstance().createTexture(size_.x, size_.y, img.getPixelsPtr());
|
||||||
|
if (smooth_) SDL2Renderer::getInstance().setTextureSmooth(textureId_, true);
|
||||||
|
if (repeated_) SDL2Renderer::getInstance().setTextureRepeated(textureId_, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1123,12 +1128,18 @@ Texture& Texture::operator=(const Texture& other) {
|
||||||
if (this != &other) {
|
if (this != &other) {
|
||||||
if (textureId_) {
|
if (textureId_) {
|
||||||
SDL2Renderer::getInstance().deleteTexture(textureId_);
|
SDL2Renderer::getInstance().deleteTexture(textureId_);
|
||||||
|
textureId_ = 0;
|
||||||
}
|
}
|
||||||
size_ = other.size_;
|
size_ = other.size_;
|
||||||
smooth_ = other.smooth_;
|
smooth_ = other.smooth_;
|
||||||
repeated_ = other.repeated_;
|
repeated_ = other.repeated_;
|
||||||
if (other.textureId_) {
|
// copyToImage() normalizes orientation, so the result is always standard.
|
||||||
textureId_ = SDL2Renderer::getInstance().createTexture(size_.x, size_.y, nullptr);
|
flippedY_ = false;
|
||||||
|
if (other.textureId_ && size_.x > 0 && size_.y > 0) {
|
||||||
|
Image img = other.copyToImage();
|
||||||
|
textureId_ = SDL2Renderer::getInstance().createTexture(size_.x, size_.y, img.getPixelsPtr());
|
||||||
|
if (smooth_) SDL2Renderer::getInstance().setTextureSmooth(textureId_, true);
|
||||||
|
if (repeated_) SDL2Renderer::getInstance().setTextureRepeated(textureId_, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
|
|
@ -1211,7 +1222,46 @@ void Texture::setRepeated(bool repeated) {
|
||||||
Image Texture::copyToImage() const {
|
Image Texture::copyToImage() const {
|
||||||
Image img;
|
Image img;
|
||||||
img.create(size_.x, size_.y);
|
img.create(size_.x, size_.y);
|
||||||
// TODO: Read back from GPU texture
|
|
||||||
|
if (!textureId_ || size_.x == 0 || size_.y == 0) {
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenGL ES 2 / WebGL doesn't have glGetTexImage.
|
||||||
|
// Workaround: attach the texture to a temporary FBO and use glReadPixels.
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId_, 0);
|
||||||
|
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
// Read RGBA pixels from the framebuffer
|
||||||
|
std::vector<Uint8> pixels(size_.x * size_.y * 4);
|
||||||
|
glReadPixels(0, 0, size_.x, size_.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
|
||||||
|
// For normal textures (loaded via stbi/loadFromImage), glReadPixels
|
||||||
|
// returns rows in the same order they were uploaded — no flip needed.
|
||||||
|
// For FBO/RenderTexture textures (flippedY_), GL renders bottom-to-top,
|
||||||
|
// so readback is scene-bottom first — flip to get conventional top-to-bottom.
|
||||||
|
if (flippedY_) {
|
||||||
|
const unsigned int rowBytes = size_.x * 4;
|
||||||
|
std::vector<Uint8> rowBuf(rowBytes);
|
||||||
|
for (unsigned int y = 0; y < size_.y / 2; ++y) {
|
||||||
|
unsigned int opposite = size_.y - 1 - y;
|
||||||
|
std::memcpy(rowBuf.data(), &pixels[y * rowBytes], rowBytes);
|
||||||
|
std::memcpy(&pixels[y * rowBytes], &pixels[opposite * rowBytes], rowBytes);
|
||||||
|
std::memcpy(&pixels[opposite * rowBytes], rowBuf.data(), rowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.create(size_.x, size_.y, pixels.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore previous framebuffer binding
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
91
tests/unit/entity_sprite_offset_test.py
Normal file
91
tests/unit/entity_sprite_offset_test.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""Test Entity sprite_offset property (#233 sub-feature 1)"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
def test(name, condition):
|
||||||
|
global passed, failed
|
||||||
|
if condition:
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
print(f"FAIL: {name}")
|
||||||
|
|
||||||
|
# Setup: create a grid and texture for entity tests
|
||||||
|
scene = mcrfpy.Scene("test")
|
||||||
|
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))
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Test 1: Default sprite_offset is (0, 0)
|
||||||
|
e = mcrfpy.Entity(grid_pos=(3, 3), texture=tex, sprite_index=0, grid=grid)
|
||||||
|
test("default sprite_offset is (0,0)",
|
||||||
|
e.sprite_offset.x == 0.0 and e.sprite_offset.y == 0.0)
|
||||||
|
|
||||||
|
# Test 2: Set sprite_offset via tuple
|
||||||
|
e.sprite_offset = (-8, -16)
|
||||||
|
test("set sprite_offset via tuple",
|
||||||
|
e.sprite_offset.x == -8.0 and e.sprite_offset.y == -16.0)
|
||||||
|
|
||||||
|
# Test 3: Read individual components
|
||||||
|
test("sprite_offset_x component", e.sprite_offset_x == -8.0)
|
||||||
|
test("sprite_offset_y component", e.sprite_offset_y == -16.0)
|
||||||
|
|
||||||
|
# Test 4: Set individual components
|
||||||
|
e.sprite_offset_x = 4.5
|
||||||
|
e.sprite_offset_y = -3.0
|
||||||
|
test("set sprite_offset_x", e.sprite_offset_x == 4.5)
|
||||||
|
test("set sprite_offset_y", e.sprite_offset_y == -3.0)
|
||||||
|
test("individual set reflects in vector",
|
||||||
|
e.sprite_offset.x == 4.5 and e.sprite_offset.y == -3.0)
|
||||||
|
|
||||||
|
# Test 5: Constructor kwarg
|
||||||
|
e2 = mcrfpy.Entity(grid_pos=(1, 1), texture=tex, sprite_index=0,
|
||||||
|
grid=grid, sprite_offset=(-8, -16))
|
||||||
|
test("constructor kwarg sprite_offset",
|
||||||
|
e2.sprite_offset.x == -8.0 and e2.sprite_offset.y == -16.0)
|
||||||
|
|
||||||
|
# Test 6: Constructor default when not specified
|
||||||
|
e3 = mcrfpy.Entity(grid_pos=(2, 2), texture=tex, sprite_index=0, grid=grid)
|
||||||
|
test("constructor default (0,0)",
|
||||||
|
e3.sprite_offset.x == 0.0 and e3.sprite_offset.y == 0.0)
|
||||||
|
|
||||||
|
# Test 7: Animation support - verify property is animatable
|
||||||
|
try:
|
||||||
|
e.animate("sprite_offset_x", -8.0, 0.5, "linear")
|
||||||
|
test("animate sprite_offset_x accepted", True)
|
||||||
|
except Exception as ex:
|
||||||
|
test(f"animate sprite_offset_x accepted: {ex}", False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
e.animate("sprite_offset_y", -16.0, 0.5, "linear")
|
||||||
|
test("animate sprite_offset_y accepted", True)
|
||||||
|
except Exception as ex:
|
||||||
|
test(f"animate sprite_offset_y accepted: {ex}", False)
|
||||||
|
|
||||||
|
# Test 8: Animation actually changes value (step the game loop)
|
||||||
|
e.sprite_offset_x = 0.0
|
||||||
|
e.animate("sprite_offset_x", 10.0, 0.1, "linear")
|
||||||
|
# Step enough to complete the animation
|
||||||
|
for _ in range(5):
|
||||||
|
mcrfpy.step(0.05)
|
||||||
|
test("animation changes sprite_offset_x", abs(e.sprite_offset_x - 10.0) < 0.1)
|
||||||
|
|
||||||
|
# Test 9: Type errors
|
||||||
|
try:
|
||||||
|
e.sprite_offset_x = "bad"
|
||||||
|
test("type error on string", False)
|
||||||
|
except TypeError:
|
||||||
|
test("type error on string", True)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n{passed} passed, {failed} failed out of {passed + failed} tests")
|
||||||
|
if failed:
|
||||||
|
print("FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("PASS")
|
||||||
|
sys.exit(0)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue