Add missing markDirty()/markCompositeDirty() to all Python property setters
Fixes a systemic bug where Python tp_getset property setters bypassed the render cache dirty flag system (#144). The animation/C++ setProperty() path had correct dirty propagation, but direct Python property assignments (e.g. frame.x = 50, caption.text = "Hello") did not invalidate the parent Frame's render cache when clip_children or cache_subtree was enabled. Changes by file: - UIDrawable.cpp: Add markCompositeDirty() to set_float_member (x/y), set_pos, set_grid_pos; add markDirty() for w/h resize - UICaption.cpp: Add markDirty() to set_text, set_color_member, set_float_member (outline/font_size); markCompositeDirty() for position - UICollection.cpp: Add markContentDirty() on owner in append, remove, pop, insert, extend, setitem, and slice assignment/deletion - UISprite.cpp: Add markDirty() to scale/sprite_index/texture setters; markCompositeDirty() to position setters - UICircle.cpp: Add markDirty() to radius/fill_color/outline_color/outline; markCompositeDirty() to center setter - UILine.cpp: Add markDirty() to start/end/color/thickness setters - UIArc.cpp: Add markDirty() to radius/angles/color/thickness setters; markCompositeDirty() to center setter - UIGrid.cpp: Add markDirty() to center/zoom/camera_rotation/fill_color/ size/perspective/fov setters Closes #288, closes #289, closes #290, closes #291 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d73a207535
commit
e58b44ef82
9 changed files with 403 additions and 14 deletions
|
|
@ -404,6 +404,7 @@ int UIArc::set_center(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -417,6 +418,7 @@ int UIArc::set_radius(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setRadius(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -430,6 +432,7 @@ int UIArc::set_start_angle(PyUIArcObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setStartAngle(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +446,7 @@ int UIArc::set_end_angle(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setEndAngle(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +466,7 @@ int UIArc::set_color(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setColor(color->data);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -475,6 +480,7 @@ int UIArc::set_thickness(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setThickness(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,14 +200,22 @@ int UICaption::set_float_member(PyUICaptionObject* self, PyObject* value, void*
|
|||
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
||||
return -1;
|
||||
}
|
||||
if (member_ptr == 0) //x
|
||||
if (member_ptr == 0) { //x
|
||||
self->data->text.setPosition(val, self->data->text.getPosition().y);
|
||||
else if (member_ptr == 1) //y
|
||||
self->data->markCompositeDirty(); // #289: position change invalidates parent cache
|
||||
}
|
||||
else if (member_ptr == 1) { //y
|
||||
self->data->text.setPosition(self->data->text.getPosition().x, val);
|
||||
else if (member_ptr == 4) //outline
|
||||
self->data->markCompositeDirty(); // #289: position change invalidates parent cache
|
||||
}
|
||||
else if (member_ptr == 4) { //outline
|
||||
self->data->text.setOutlineThickness(val);
|
||||
else if (member_ptr == 5) // character size
|
||||
self->data->markDirty(); // #289: content change invalidates own + parent cache
|
||||
}
|
||||
else if (member_ptr == 5) { // character size
|
||||
self->data->text.setCharacterSize(val);
|
||||
self->data->markDirty(); // #289: content change invalidates own + parent cache
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +227,7 @@ PyObject* UICaption::get_vec_member(PyUICaptionObject* self, void* closure)
|
|||
int UICaption::set_vec_member(PyUICaptionObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
self->data->text.setPosition(PyVector::fromPy(value));
|
||||
self->data->markCompositeDirty(); // #289: position change invalidates parent cache
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -289,10 +298,12 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
|
|||
if (member_ptr == 0)
|
||||
{
|
||||
self->data->text.setFillColor(sf::Color(r, g, b, a));
|
||||
self->data->markDirty(); // #289: color change invalidates own + parent cache
|
||||
}
|
||||
else if (member_ptr == 1)
|
||||
{
|
||||
self->data->text.setOutlineColor(sf::Color(r, g, b, a));
|
||||
self->data->markDirty(); // #289: color change invalidates own + parent cache
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -329,6 +340,7 @@ int UICaption::set_text(PyUICaptionObject* self, PyObject* value, void* closure)
|
|||
Py_DECREF(temp_bytes);
|
||||
}
|
||||
self->data->text.setString(Resources::caption_buffer);
|
||||
self->data->markDirty(); // #289: text change invalidates own + parent cache
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ int UICircle::set_radius(PyUICircleObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setRadius(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -355,6 +356,7 @@ int UICircle::set_center(PyUICircleObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -382,6 +384,7 @@ int UICircle::set_fill_color(PyUICircleObject* self, PyObject* value, void* clos
|
|||
return -1;
|
||||
}
|
||||
self->data->setFillColor(color);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +412,7 @@ int UICircle::set_outline_color(PyUICircleObject* self, PyObject* value, void* c
|
|||
return -1;
|
||||
}
|
||||
self->data->setOutlineColor(color);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -422,6 +426,7 @@ int UICircle::set_outline(PyUICircleObject* self, PyObject* value, void* closure
|
|||
return -1;
|
||||
}
|
||||
self->data->setOutline(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,11 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
|||
// #122: Clear the parent before removing
|
||||
(*self->data)[index]->setParent(nullptr);
|
||||
self->data->erase(self->data->begin() + index);
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +307,12 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
|||
// Mark scene as needing resort after replacing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -494,7 +505,14 @@ int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObj
|
|||
|
||||
// Mark scene as needing resort after slice deletion
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
{
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
// Assignment
|
||||
|
|
@ -564,7 +582,14 @@ int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObj
|
|||
|
||||
// Mark scene as needing resort after slice assignment
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
{
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -635,6 +660,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
// Mark scene as needing resort after adding element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
|
@ -689,7 +720,12 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
|||
|
||||
// Mark scene as needing resort after adding elements
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
|
@ -717,6 +753,11 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
|
|||
(*it)->setParent(nullptr);
|
||||
vec->erase(it);
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
}
|
||||
|
|
@ -766,6 +807,12 @@ PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args)
|
|||
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
// Convert to Python object and return
|
||||
return convertDrawableToPython(drawable);
|
||||
}
|
||||
|
|
@ -817,6 +864,12 @@ PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args)
|
|||
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr2 = self->owner.lock();
|
||||
if (owner_ptr2) {
|
||||
owner_ptr2->markContentDirty();
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -545,10 +545,12 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure)
|
|||
case 0: // x
|
||||
drawable->position.x = val;
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
break;
|
||||
case 1: // y
|
||||
drawable->position.y = val;
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
break;
|
||||
case 2: // w
|
||||
case 3: // h
|
||||
|
|
@ -559,6 +561,7 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure)
|
|||
} else {
|
||||
drawable->resize(bounds.width, val);
|
||||
}
|
||||
drawable->markDirty(); // #290: size change invalidates own + parent cache
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
@ -638,6 +641,7 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
|
||||
drawable->position = sf::Vector2f(x, y);
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -873,6 +877,7 @@ int UIDrawable::set_grid_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
drawable->position.x = grid_x * cell_size.x;
|
||||
drawable->position.y = grid_y * cell_size.y;
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1074,7 +1074,8 @@ int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) {
|
|||
tex_height = std::min(tex_height, 4096u);
|
||||
|
||||
self->data->renderTexture.create(tex_width, tex_height);
|
||||
|
||||
self->data->markDirty(); // #291: size change
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1091,6 +1092,7 @@ int UIGrid::set_center(PyUIGridObject* self, PyObject* value, void* closure) {
|
|||
}
|
||||
self->data->center_x = x;
|
||||
self->data->center_y = y;
|
||||
self->data->markDirty(); // #291: camera position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1186,6 +1188,14 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
|
|||
else if (member_ptr == 7) self->view->camera_rotation = val;
|
||||
self->view->position = self->view->box.getPosition();
|
||||
}
|
||||
|
||||
// #291: Dirty flag propagation for visual property changes
|
||||
if (member_ptr == 0 || member_ptr == 1) {
|
||||
self->data->markCompositeDirty(); // position change
|
||||
} else {
|
||||
self->data->markDirty(); // content/size change
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
// TODO (7DRL Day 2, item 5.) return Texture object
|
||||
|
|
@ -1310,6 +1320,7 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
|
||||
PyColorObject* color = (PyColorObject*)value;
|
||||
self->data->fill_color = color->data;
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1342,6 +1353,7 @@ int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure
|
|||
if (value == Py_None) {
|
||||
// Clear perspective but keep perspective_enabled unchanged
|
||||
self->data->perspective_entity.reset();
|
||||
self->data->markDirty(); // #291: FOV rendering change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1354,6 +1366,7 @@ int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure
|
|||
PyUIEntityObject* entity_obj = (PyUIEntityObject*)value;
|
||||
self->data->perspective_entity = entity_obj->data;
|
||||
self->data->perspective_enabled = true; // Enable perspective when entity assigned
|
||||
self->data->markDirty(); // #291: FOV rendering change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1369,6 +1382,7 @@ int UIGrid::set_perspective_enabled(PyUIGridObject* self, PyObject* value, void*
|
|||
return -1; // Error occurred
|
||||
}
|
||||
self->data->perspective_enabled = enabled;
|
||||
self->data->markDirty(); // #291: FOV rendering toggle
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1401,6 +1415,7 @@ int UIGrid::set_fov(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->fov_algorithm = algo;
|
||||
self->data->markDirty(); // #291: FOV algorithm change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1425,6 +1440,7 @@ int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->fov_radius = (int)radius;
|
||||
self->data->markDirty(); // #291: FOV radius change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -423,6 +423,7 @@ int UILine::set_start(PyUILineObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setStart(vec->data);
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +444,7 @@ int UILine::set_end(PyUILineObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setEnd(vec->data);
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +464,7 @@ int UILine::set_color(PyUILineObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setColor(color->data);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -486,6 +489,7 @@ int UILine::set_thickness(PyUILineObject* self, PyObject* value, void* closure)
|
|||
}
|
||||
|
||||
self->data->setThickness(thickness);
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -285,16 +285,26 @@ int UISprite::set_float_member(PyUISpriteObject* self, PyObject* value, void* cl
|
|||
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
||||
return -1;
|
||||
}
|
||||
if (member_ptr == 0) //x
|
||||
if (member_ptr == 0) { //x
|
||||
self->data->setPosition(sf::Vector2f(val, self->data->getPosition().y));
|
||||
else if (member_ptr == 1) //y
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
}
|
||||
else if (member_ptr == 1) { //y
|
||||
self->data->setPosition(sf::Vector2f(self->data->getPosition().x, val));
|
||||
else if (member_ptr == 2) // scale (uniform)
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
}
|
||||
else if (member_ptr == 2) { // scale (uniform)
|
||||
self->data->setScale(sf::Vector2f(val, val));
|
||||
else if (member_ptr == 3) // scale_x
|
||||
self->data->markDirty(); // #291: visual change
|
||||
}
|
||||
else if (member_ptr == 3) { // scale_x
|
||||
self->data->setScale(sf::Vector2f(val, self->data->getScale().y));
|
||||
else if (member_ptr == 4) // scale_y
|
||||
self->data->markDirty(); // #291: visual change
|
||||
}
|
||||
else if (member_ptr == 4) { // scale_y
|
||||
self->data->setScale(sf::Vector2f(self->data->getScale().x, val));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -339,6 +349,7 @@ int UISprite::set_int_member(PyUISpriteObject* self, PyObject* value, void* clos
|
|||
}
|
||||
|
||||
self->data->setSpriteIndex(val);
|
||||
self->data->markDirty(); // #291: sprite content change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -364,7 +375,8 @@ int UISprite::set_texture(PyUISpriteObject* self, PyObject* value, void* closure
|
|||
|
||||
// Update the sprite's texture
|
||||
self->data->setTexture(pytexture->data);
|
||||
|
||||
self->data->markDirty(); // #291: texture change
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -387,6 +399,7 @@ int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setPosition(vec->data);
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
275
tests/regression/issue_288_291_dirty_flags_test.py
Normal file
275
tests/regression/issue_288_291_dirty_flags_test.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
"""Test render cache dirty flag propagation for issues #288-#291.
|
||||
|
||||
#288: UICollection mutations don't invalidate parent Frame's render cache
|
||||
#289: Caption Python property setters don't call markDirty()
|
||||
#290: UIDrawable base x/y/pos setters don't propagate dirty flags to parent
|
||||
#291: Audit all Python property setters for missing markDirty() calls
|
||||
|
||||
These tests exercise all property setters that were missing dirty flag calls,
|
||||
inside a clip_children=True Frame (which uses render caching). The test verifies
|
||||
that no crashes occur and properties are correctly set after modification.
|
||||
Visual correctness requires a non-headless render test.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
test_pass = True
|
||||
test_count = 0
|
||||
fail_count = 0
|
||||
|
||||
def check(condition, msg):
|
||||
global test_pass, test_count, fail_count
|
||||
test_count += 1
|
||||
if not condition:
|
||||
print(f" FAIL: {msg}")
|
||||
test_pass = False
|
||||
fail_count += 1
|
||||
|
||||
# Create a scene with a clipped parent frame (uses render caching)
|
||||
scene = mcrfpy.Scene("test_dirty_flags")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
parent = mcrfpy.Frame(pos=(10, 10), size=(800, 600),
|
||||
fill_color=mcrfpy.Color(40, 40, 40),
|
||||
clip_children=True)
|
||||
scene.children.append(parent)
|
||||
|
||||
# ============================================================
|
||||
# Test #290: UIDrawable base x/y/pos setters (all drawable types)
|
||||
# ============================================================
|
||||
print("Testing #290: UIDrawable position setters...")
|
||||
|
||||
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100),
|
||||
fill_color=mcrfpy.Color(255, 0, 0))
|
||||
parent.children.append(frame)
|
||||
|
||||
# Test x setter
|
||||
frame.x = 50.0
|
||||
check(frame.x == 50.0, "frame.x setter")
|
||||
|
||||
# Test y setter
|
||||
frame.y = 60.0
|
||||
check(frame.y == 60.0, "frame.y setter")
|
||||
|
||||
# Test pos setter (tuple)
|
||||
frame.pos = (70.0, 80.0)
|
||||
check(frame.x == 70.0 and frame.y == 80.0, "frame.pos setter (tuple)")
|
||||
|
||||
# Test w/h setters
|
||||
frame.w = 200.0
|
||||
check(frame.w == 200.0, "frame.w setter")
|
||||
frame.h = 150.0
|
||||
check(frame.h == 150.0, "frame.h setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #289: Caption property setters
|
||||
# ============================================================
|
||||
print("Testing #289: Caption property setters...")
|
||||
|
||||
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
|
||||
parent.children.append(cap)
|
||||
|
||||
# Text setter
|
||||
cap.text = "World"
|
||||
check(cap.text == "World", "caption.text setter")
|
||||
|
||||
# Fill color setter
|
||||
cap.fill_color = mcrfpy.Color(255, 0, 0)
|
||||
c = cap.fill_color
|
||||
check(c.r == 255 and c.g == 0 and c.b == 0, "caption.fill_color setter")
|
||||
|
||||
# Outline color setter
|
||||
cap.outline_color = mcrfpy.Color(0, 255, 0)
|
||||
c = cap.outline_color
|
||||
check(c.r == 0 and c.g == 255 and c.b == 0, "caption.outline_color setter")
|
||||
|
||||
# Outline thickness setter
|
||||
cap.outline = 2.0
|
||||
check(cap.outline == 2.0, "caption.outline setter")
|
||||
|
||||
# Font size setter
|
||||
cap.font_size = 24
|
||||
check(cap.font_size == 24, "caption.font_size setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #288: UICollection mutations
|
||||
# ============================================================
|
||||
print("Testing #288: UICollection mutations...")
|
||||
|
||||
# append (already tested above, but test with clip_children parent)
|
||||
child1 = mcrfpy.Frame(pos=(0, 0), size=(20, 20),
|
||||
fill_color=mcrfpy.Color(0, 0, 255))
|
||||
initial_count = len(parent.children)
|
||||
parent.children.append(child1)
|
||||
check(len(parent.children) == initial_count + 1, "collection append")
|
||||
|
||||
# insert
|
||||
child2 = mcrfpy.Frame(pos=(30, 0), size=(20, 20),
|
||||
fill_color=mcrfpy.Color(0, 255, 0))
|
||||
parent.children.insert(0, child2)
|
||||
check(len(parent.children) == initial_count + 2, "collection insert")
|
||||
|
||||
# setitem (replace)
|
||||
child3 = mcrfpy.Frame(pos=(60, 0), size=(20, 20),
|
||||
fill_color=mcrfpy.Color(255, 255, 0))
|
||||
parent.children[0] = child3
|
||||
check(len(parent.children) == initial_count + 2, "collection setitem")
|
||||
|
||||
# remove
|
||||
parent.children.remove(child1)
|
||||
check(len(parent.children) == initial_count + 1, "collection remove")
|
||||
|
||||
# pop
|
||||
popped = parent.children.pop()
|
||||
check(len(parent.children) == initial_count, "collection pop")
|
||||
|
||||
# extend
|
||||
extras = [
|
||||
mcrfpy.Frame(pos=(0, 200), size=(20, 20), fill_color=mcrfpy.Color(128, 128, 128)),
|
||||
mcrfpy.Frame(pos=(30, 200), size=(20, 20), fill_color=mcrfpy.Color(64, 64, 64))
|
||||
]
|
||||
parent.children.extend(extras)
|
||||
check(len(parent.children) == initial_count + 2, "collection extend")
|
||||
|
||||
# slice deletion
|
||||
del parent.children[initial_count:]
|
||||
check(len(parent.children) == initial_count, "collection slice delete")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UISprite property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UISprite property setters...")
|
||||
|
||||
# Need a texture for sprite tests - use a test texture if available
|
||||
try:
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
sprite = mcrfpy.Sprite(pos=(200, 200), texture=tex, sprite_index=0)
|
||||
parent.children.append(sprite)
|
||||
|
||||
sprite.scale = 2.0
|
||||
check(sprite.scale == 2.0, "sprite.scale setter")
|
||||
|
||||
sprite.sprite_index = 1
|
||||
check(sprite.sprite_index == 1, "sprite.sprite_index setter")
|
||||
|
||||
# Texture setter
|
||||
sprite.texture = tex
|
||||
check(True, "sprite.texture setter (no crash)")
|
||||
|
||||
# Pos setter
|
||||
sprite.pos = (210, 210)
|
||||
check(True, "sprite.pos setter (no crash)")
|
||||
except Exception as e:
|
||||
print(f" (Sprite tests skipped - no test texture: {e})")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UICircle property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UICircle property setters...")
|
||||
|
||||
circle = mcrfpy.Circle(radius=25.0, center=(300, 300),
|
||||
fill_color=mcrfpy.Color(255, 128, 0))
|
||||
parent.children.append(circle)
|
||||
|
||||
circle.radius = 30.0
|
||||
check(circle.radius == 30.0, "circle.radius setter")
|
||||
|
||||
circle.fill_color = mcrfpy.Color(0, 128, 255)
|
||||
c = circle.fill_color
|
||||
check(c.r == 0 and c.g == 128 and c.b == 255, "circle.fill_color setter")
|
||||
|
||||
circle.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
c = circle.outline_color
|
||||
check(c.r == 255, "circle.outline_color setter")
|
||||
|
||||
circle.outline = 3.0
|
||||
check(circle.outline == 3.0, "circle.outline setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UILine property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UILine property setters...")
|
||||
|
||||
line = mcrfpy.Line(start=(10, 400), end=(200, 400),
|
||||
thickness=2.0, color=mcrfpy.Color(255, 0, 255))
|
||||
parent.children.append(line)
|
||||
|
||||
line.start = (20, 410)
|
||||
check(True, "line.start setter (no crash)")
|
||||
|
||||
line.end = (210, 410)
|
||||
check(True, "line.end setter (no crash)")
|
||||
|
||||
line.color = mcrfpy.Color(0, 255, 255)
|
||||
c = line.color
|
||||
check(c.r == 0 and c.g == 255 and c.b == 255, "line.color setter")
|
||||
|
||||
line.thickness = 4.0
|
||||
check(line.thickness == 4.0, "line.thickness setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UIArc property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UIArc property setters...")
|
||||
|
||||
arc = mcrfpy.Arc(center=(400, 300), radius=40.0, start_angle=0.0,
|
||||
end_angle=180.0, color=mcrfpy.Color(128, 0, 255),
|
||||
thickness=3.0)
|
||||
parent.children.append(arc)
|
||||
|
||||
arc.radius = 50.0
|
||||
check(arc.radius == 50.0, "arc.radius setter")
|
||||
|
||||
arc.start_angle = 45.0
|
||||
check(arc.start_angle == 45.0, "arc.start_angle setter")
|
||||
|
||||
arc.end_angle = 270.0
|
||||
check(arc.end_angle == 270.0, "arc.end_angle setter")
|
||||
|
||||
arc.color = mcrfpy.Color(255, 128, 128)
|
||||
c = arc.color
|
||||
check(c.r == 255 and c.g == 128 and c.b == 128, "arc.color setter")
|
||||
|
||||
arc.thickness = 5.0
|
||||
check(arc.thickness == 5.0, "arc.thickness setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UIGrid property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UIGrid property setters...")
|
||||
|
||||
try:
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(500, 100), size=(200, 200))
|
||||
parent.children.append(grid)
|
||||
|
||||
grid.center_x = 5.0
|
||||
check(True, "grid.center_x setter (no crash)")
|
||||
|
||||
grid.center_y = 5.0
|
||||
check(True, "grid.center_y setter (no crash)")
|
||||
|
||||
grid.zoom = 2.0
|
||||
check(grid.zoom == 2.0, "grid.zoom setter")
|
||||
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 40)
|
||||
check(True, "grid.fill_color setter (no crash)")
|
||||
except Exception as e:
|
||||
print(f" (Grid tests skipped: {e})")
|
||||
|
||||
# ============================================================
|
||||
# Trigger a render cycle to exercise dirty flag code paths
|
||||
# ============================================================
|
||||
print("Triggering render cycle...")
|
||||
mcrfpy.step(0.016) # ~1 frame at 60fps
|
||||
|
||||
# ============================================================
|
||||
# Summary
|
||||
# ============================================================
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Results: {test_count - fail_count}/{test_count} passed")
|
||||
if test_pass:
|
||||
print("PASS: All dirty flag propagation tests passed")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"FAIL: {fail_count} test(s) failed")
|
||||
sys.exit(1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue