From 95463bdc78476924a175289a83270867b6511ecc Mon Sep 17 00:00:00 2001 From: John McCardle Date: Thu, 9 Apr 2026 21:18:47 -0400 Subject: [PATCH] Add Color.__eq__/__ne__ for value comparison, closes #307 Color had __hash__ but no __eq__/__ne__, violating the Python convention that hashable objects must support equality comparison. Two Color objects with identical RGBA values would not compare equal. Now supports comparison with Color objects, tuples, and lists: Color(255, 0, 0) == Color(255, 0, 0) # True Color(255, 0, 0) == (255, 0, 0) # True Color(255, 0, 0) != (0, 0, 0) # True Co-Authored-By: Claude Opus 4.6 --- src/PyColor.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++------ src/PyColor.h | 2 ++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/PyColor.cpp b/src/PyColor.cpp index f09a6fe..1b12e29 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -367,36 +367,92 @@ PyObject* PyColor::lerp(PyColorObject* self, PyObject* args) { PyObject* other_obj; float t; - + if (!PyArg_ParseTuple(args, "Of", &other_obj, &t)) { return NULL; } - + // Validate other color if (!PyObject_IsInstance(other_obj, (PyObject*)&mcrfpydef::PyColorType)) { PyErr_SetString(PyExc_TypeError, "First argument must be a Color"); return NULL; } - + PyColorObject* other = (PyColorObject*)other_obj; - + // Clamp t to [0, 1] if (t < 0.0f) t = 0.0f; if (t > 1.0f) t = 1.0f; - + // Perform linear interpolation sf::Uint8 r = static_cast(self->data.r + (other->data.r - self->data.r) * t); sf::Uint8 g = static_cast(self->data.g + (other->data.g - self->data.g) * t); sf::Uint8 b = static_cast(self->data.b + (other->data.b - self->data.b) * t); sf::Uint8 a = static_cast(self->data.a + (other->data.a - self->data.a) * t); - + // Create new Color object auto type = &mcrfpydef::PyColorType; PyColorObject* result = (PyColorObject*)type->tp_alloc(type, 0); - + if (result) { result->data = sf::Color(r, g, b, a); } - + return (PyObject*)result; } + +PyObject* PyColor::richcompare(PyObject* left, PyObject* right, int op) +{ + // Only support == and != + if (op != Py_EQ && op != Py_NE) { + Py_RETURN_NOTIMPLEMENTED; + } + + // Extract RGBA from left operand + sf::Color left_c, right_c; + + if (PyObject_IsInstance(left, (PyObject*)&mcrfpydef::PyColorType)) { + left_c = ((PyColorObject*)left)->data; + } else { + Py_RETURN_NOTIMPLEMENTED; + } + + // Extract RGBA from right operand - accept Color, tuple, or list + if (PyObject_IsInstance(right, (PyObject*)&mcrfpydef::PyColorType)) { + right_c = ((PyColorObject*)right)->data; + } else if (PyTuple_Check(right) || PyList_Check(right)) { + Py_ssize_t size = PySequence_Size(right); + if (size < 3 || size > 4) { + Py_RETURN_NOTIMPLEMENTED; + } + PyObject* r = PySequence_GetItem(right, 0); + PyObject* g = PySequence_GetItem(right, 1); + PyObject* b = PySequence_GetItem(right, 2); + if (!PyLong_Check(r) || !PyLong_Check(g) || !PyLong_Check(b)) { + Py_DECREF(r); Py_DECREF(g); Py_DECREF(b); + Py_RETURN_NOTIMPLEMENTED; + } + right_c.r = (sf::Uint8)PyLong_AsLong(r); + right_c.g = (sf::Uint8)PyLong_AsLong(g); + right_c.b = (sf::Uint8)PyLong_AsLong(b); + Py_DECREF(r); Py_DECREF(g); Py_DECREF(b); + if (size == 4) { + PyObject* a = PySequence_GetItem(right, 3); + if (PyLong_Check(a)) right_c.a = (sf::Uint8)PyLong_AsLong(a); + Py_DECREF(a); + } else { + right_c.a = 255; + } + } else { + Py_RETURN_NOTIMPLEMENTED; + } + + bool equal = (left_c.r == right_c.r && left_c.g == right_c.g && + left_c.b == right_c.b && left_c.a == right_c.a); + + if (op == Py_EQ) { + return PyBool_FromLong(equal); + } else { + return PyBool_FromLong(!equal); + } +} diff --git a/src/PyColor.h b/src/PyColor.h index 601090c..8701cdd 100644 --- a/src/PyColor.h +++ b/src/PyColor.h @@ -23,6 +23,7 @@ public: static sf::Color fromPy(PyColorObject*); static PyObject* repr(PyObject*); static Py_hash_t hash(PyObject*); + static PyObject* richcompare(PyObject*, PyObject*, int); static int init(PyColorObject*, PyObject*, PyObject*); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* get_member(PyObject*, void*); @@ -75,6 +76,7 @@ namespace mcrfpydef { " anim = mcrfpy.Animation('fill_color.r', 255, 0.5, 'linear')\n" " anim.start(frame)\n" ), + .tp_richcompare = PyColor::richcompare, .tp_methods = PyColor::methods, .tp_getset = PyColor::getsetters, .tp_init = (initproc)PyColor::init,