From f1b354e47d199ec736f3009c14c6e9cbdf1dd342 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 6 Jul 2025 00:13:39 -0400 Subject: [PATCH] feat: Phase 1 - safe constructors and _Drawable foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #7 - Make all UI class constructors safe: - Added safe default constructors for UISprite, UIGrid, UIEntity, UICaption - Initialize all members to predictable values - Made Python init functions accept no arguments - Added x,y properties to UIEntity Closes #71 - Create _Drawable Python base class: - Created PyDrawable.h/cpp with base type (not yet inherited by UI types) - Registered in module initialization Closes #87 - Add visible property: - Added bool visible=true to UIDrawable base class - All render methods check visibility before drawing Closes #88 - Add opacity property: - Added float opacity=1.0 to UIDrawable base class - UICaption and UISprite apply opacity to alpha channel Closes #89 - Add get_bounds() method: - Virtual method returns sf::FloatRect(x,y,w,h) - Implemented in Frame, Caption, Sprite, Grid Closes #98 - Add move() and resize() methods: - move(dx,dy) for relative movement - resize(w,h) for absolute sizing - Caption resize is no-op (size controlled by font) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- ROADMAP.md | 141 +++++------------------------------ src/McRFPy_API.cpp | 7 +- src/PyDrawable.cpp | 179 +++++++++++++++++++++++++++++++++++++++++++++ src/PyDrawable.h | 15 ++++ src/UICaption.cpp | 70 +++++++++++++++--- src/UICaption.h | 6 ++ src/UIDrawable.h | 9 +++ src/UIEntity.cpp | 109 +++++++++++++++++++++++---- src/UIEntity.h | 2 + src/UIFrame.cpp | 43 ++++++++--- src/UIFrame.h | 5 ++ src/UIGrid.cpp | 54 +++++++++++++- src/UIGrid.h | 5 ++ src/UISprite.cpp | 42 ++++++++++- src/UISprite.h | 5 ++ 15 files changed, 531 insertions(+), 161 deletions(-) create mode 100644 src/PyDrawable.cpp create mode 100644 src/PyDrawable.h diff --git a/ROADMAP.md b/ROADMAP.md index 9a08f7a..0d1f9a8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -155,7 +155,7 @@ Rendering Layer: - from_hex("#FF0000"), to_hex() - lerp(other_color, t) for interpolation -4. NEW - Timer objects +4. #103 - Timer objects timer = mcrfpy.Timer("my_timer", callback, 1000) timer.pause() timer.resume() @@ -180,7 +180,7 @@ Rendering Layer: - scene.find("button1") returns element - collection.find("enemy*") returns list -4. NEW - Basic profiling/metrics +4. #104 - Basic profiling/metrics - Frame time tracking - Draw call counting - Python vs C++ time split @@ -211,7 +211,7 @@ Rendering Layer: - Option 2: mcrfpy.sfml submodule - Option 3: Direct integration -5. NEW - Scene transitions +5. 105 - Scene transitions scene.fade_to(next_scene, duration=1.0) scene.slide_out(direction="left") ``` @@ -232,10 +232,10 @@ Rendering Layer: 3. #50 - Grid background colors grid.background_color = mcrfpy.Color(50, 50, 50) -4. NEW - Shader support (stretch goal) +4. #106 - Shader support sprite.shader = "glow.frag" -5. NEW - Particle system (stretch goal) +5. #107 - Particle system particles = mcrfpy.ParticleEmitter() ``` *Rationale*: This unlocks professional visual effects but is complex. @@ -245,7 +245,7 @@ Rendering Layer: ``` 1. #85 - Replace all "docstring" placeholders 2. #86 - Add parameter documentation -3. Generate .pyi type stubs for IDE support +3. #108 - Generate .pyi type stubs for IDE support 4. #70 - PyPI wheel preparation 5. API reference generator tool ``` @@ -259,7 +259,7 @@ Rendering Layer: **Track A: Entity Systems** - Entity/Grid integration (#30) -- Timer objects (NEW) +- Timer objects (#103) - Vector/Color helpers (#93, #94) **Track B: API Polish** @@ -270,7 +270,7 @@ Rendering Layer: **Track C: Performance** - Grid culling (#52) - Visibility culling (part of #10) -- Profiling tools (NEW) +- Profiling tools (#104) ### 💎 **Quick Wins to Sprinkle Throughout** 1. Color helpers (#94) - 1 hour @@ -289,14 +289,14 @@ Rendering Layer: ### 🆕 **New Issues to Create** -1. **Timer Objects** - Pythonic timer management +1. **Timer Objects** - Pythonic timer management (#103) 2. **Event System Enhancement** - Mouse enter/leave, drag, right-click 3. **Resource Manager** - Centralized asset loading 4. **Serialization System** - Save/load game state -5. **Scene Transitions** - Fade, slide, custom effects -6. **Profiling Tools** - Performance metrics -7. **Particle System** - Visual effects framework -8. **Shader Support** - Custom rendering effects +5. **Scene Transitions** - Fade, slide, custom effects (#105) +6. **Profiling Tools** - Performance metrics (#104) +7. **Particle System** - Visual effects framework (#107) +8. **Shader Support** - Custom rendering effects (#106) --- @@ -312,6 +312,7 @@ Rendering Layer: - [x] **#77** - Fix error message copy/paste bug - *Fixed* - [x] **#74** - Add missing `Grid.grid_y` property - *Fixed* - [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix* + Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway. - [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed* - [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed* - [x] **keypressScene() Validation** - Add proper error handling - *Fixed* @@ -327,21 +328,6 @@ Rendering Layer: --- -## ✅ ALPHA 0.1 RELEASE ACHIEVED! (All Blockers Complete) - -### ✅ All Alpha Requirements Complete! -- [x] **#69** - Collections use Python Sequence Protocol - *Completed! (2025-07-05)* -- [x] **#63** - Z-order rendering for UIDrawables - *Completed! (2025-07-05)* -- [x] **#59** - Animation system for arbitrary UIDrawable fields - *Completed! (2025-07-05)* -- [x] **#47** - New README.md for Alpha release - *Completed* -- [x] **#3** - Remove deprecated `McRFPy_API::player_input` - *Completed* -- [x] **#2** - Remove `registerPyAction` system - *Completed* - -### 📋 Moved to Beta: -- [ ] **#6** - RenderTexture concept - *Moved to Beta (not needed for Alpha)* - ---- - ## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues) ### 🎮 Core Engine Systems @@ -351,7 +337,7 @@ Rendering Layer: - [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)* #### Python/C++ Integration (7 issues) -- [ ] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations* +- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations* - [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul* - [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul* - [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)* @@ -360,12 +346,12 @@ Rendering Layer: - [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations* #### UI/Rendering System (12 issues) -- [ ] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations* +- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations* - [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)* - [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul* - [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul* - [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations* -- [ ] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations* +- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations* - [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix* - [ ] **#50** - UIGrid background color field - *Isolated Fix* - [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations* @@ -378,7 +364,7 @@ Rendering Layer: - [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul* - [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations* - [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations* -- [ ] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix* +- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix* #### Scene/Window Management (5 issues) - [ ] **#61** - Scene object encapsulating key callbacks - *Extensive Overhaul* @@ -411,7 +397,7 @@ Rendering Layer: ### 📚 Demo & Documentation #### Documentation (2 issues) -- [ ] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix* +- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix* - [ ] **#48** - Dependency compilation documentation - *Isolated Fix* #### Demo Projects (6 issues) @@ -424,83 +410,6 @@ Rendering Layer: --- -## 🎯 RECOMMENDED TRIAGE SEQUENCE - -### Phase 1: Foundation Stabilization (1-2 weeks) -``` -✅ COMPLETE AS OF 2025-01-03: -1. ✅ Fix Grid Segfault - Grid now supports None/null textures -2. ✅ Fix #78 Middle Mouse Click bug - Event type checking added -3. ✅ Fix Entity/Sprite property setters - PyVector conversion fixed -4. ✅ Fix #77 - Error message copy/paste bug fixed -5. ✅ Fix #74 - Grid.grid_y property added -6. ✅ Fix keypressScene() validation - Now rejects non-callable -7. ✅ Fix Sprite texture setter - No longer returns error without exception -8. ✅ Fix PyVector x/y properties - Were returning None - -REMAINING IN PHASE 1: -9. ✅ Fix #73 - Entity.index() method for removal -10. ✅ Fix #27 - EntityCollection.extend() method -11. ✅ Fix #33 - Sprite index validation -12. Alpha Blockers (#3, #2) - Remove deprecated methods -``` - -### Phase 2: Alpha Release Preparation (4-6 weeks) -``` -1. Collections Sequence Protocol (#69) - Major refactor, alpha blocker -2. Z-order rendering (#63) - Essential UI improvement, alpha blocker -3. RenderTexture overhaul (#6) - Core rendering improvement, alpha blocker -4. ✅ Animation system (#59) - COMPLETE! 30+ easing functions, all UI properties -5. ✅ Documentation (#47) - README.md complete, #48 dependency docs remaining -``` - -### Phase 3: Engine Architecture (6-8 weeks) -``` -1. Drawable base class (#71) - Clean up inheritance patterns -2. Entity/Grid associations (#30) - Proper lifecycle management -3. Window object (#34) - Scene/window architecture -4. UIDrawable visibility (#10) - Rendering optimization -``` - -### Phase 4: Advanced Features (8-12 weeks) -``` -1. Grid strict mode (#16) - Entity knowledge/visibility system -2. SFML/TCOD integration (#14, #35) - Expose native libraries -3. Scene object refactor (#61) - Better input handling -4. Name-based finding (#39, #40, #41) - UI element management -5. Demo projects (#54, #55, #36) - Showcase capabilities -``` - -### Ongoing/Low Priority -``` -- PyPI distribution (#70) - Community access -- Multiple windows (#62) - Advanced use cases -- Grid stitching (#67) - Infinite world support -- Accessibility (#45) - Important but not blocking -- Subinterpreter tests (#46) - Performance research -``` - ---- - -## 📊 DIFFICULTY ASSESSMENT SUMMARY - -**Isolated Fixes (24 issues)**: Single file/function changes -- Bugfixes: #77, #74, #37, #78 -- Simple features: #73, #52, #50, #33, #17, #38, #42, #27, #28, #26, #12, #1 -- Cleanup: #3, #2, #21, #47, #48 - -**Multiple Integrations (28 issues)**: Cross-system changes -- UI/Rendering: #63, #8, #9, #19, #39, #40, #41 -- Grid/Entity: #15, #20, #76, #46, #49, #75 -- Features: #54, #55, #53, #45, #7 - -**Extensive Overhauls (26 issues)**: Major architectural changes -- Core Systems: #69, #59, #6, #10, #30, #16, #67, #61, #34, #62 -- Integration: #71, #70, #32, #35, #14 -- Advanced: #36, #65 - ---- - ## 🎮 STRATEGIC DIRECTION ### Engine Philosophy Maintained @@ -514,13 +423,6 @@ REMAINING IN PHASE 1: 3. **Resource Management**: RAII everywhere, proper lifecycle handling 4. **Multi-Platform**: Windows/Linux feature parity maintained -### Success Metrics for Alpha 0.1 -- [ ] All Alpha Blocker issues resolved (5 of 7 complete: #69, #59, #47, #3, #2) -- [ ] Grid point iteration complete and tested -- [ ] Clean build on Windows and Linux -- [ ] Documentation sufficient for external developers -- [ ] At least one compelling demo (Wumpus or Jupyter integration) - --- ## 📚 REFERENCES & CONTEXT @@ -544,8 +446,3 @@ REMAINING IN PHASE 1: --- *Last Updated: 2025-07-05* -*Total Open Issues: 62* (from original 78) -*Alpha Status: 🎉 COMPLETE! All blockers resolved!* -*Achievement Unlocked: Alpha 0.1 Release Ready* -*Next Phase: Beta features including RenderTexture (#6), advanced UI patterns, and platform polish* - diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index a792150..a1ed25f 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -2,6 +2,7 @@ #include "McRFPy_Automation.h" #include "platform.h" #include "PyAnimation.h" +#include "PyDrawable.h" #include "GameEngine.h" #include "UI.h" #include "Resources.h" @@ -69,6 +70,9 @@ PyObject* PyInit_mcrfpy() /*SFML exposed types*/ &PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType, + /*Base classes*/ + &PyDrawableType, + /*UI widgets*/ &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, @@ -100,8 +104,7 @@ PyObject* PyInit_mcrfpy() // Add default_font and default_texture to module McRFPy_API::default_font = std::make_shared("assets/JetbrainsMono.ttf"); McRFPy_API::default_texture = std::make_shared("assets/kenney_tinydungeon.png", 16, 16); - //PyModule_AddObject(m, "default_font", McRFPy_API::default_font->pyObject()); - //PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject()); + // These will be set later when the window is created PyModule_AddObject(m, "default_font", Py_None); PyModule_AddObject(m, "default_texture", Py_None); diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp new file mode 100644 index 0000000..9648335 --- /dev/null +++ b/src/PyDrawable.cpp @@ -0,0 +1,179 @@ +#include "PyDrawable.h" +#include "McRFPy_API.h" + +// Click property getter +static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) +{ + if (!self->data->click_callable) + Py_RETURN_NONE; + + PyObject* ptr = self->data->click_callable->borrow(); + if (ptr && ptr != Py_None) + return ptr; + else + Py_RETURN_NONE; +} + +// Click property setter +static int PyDrawable_set_click(PyDrawableObject* self, PyObject* value, void* closure) +{ + if (value == Py_None) { + self->data->click_unregister(); + } else if (PyCallable_Check(value)) { + self->data->click_register(value); + } else { + PyErr_SetString(PyExc_TypeError, "click must be callable or None"); + return -1; + } + return 0; +} + +// Z-index property getter +static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure) +{ + return PyLong_FromLong(self->data->z_index); +} + +// Z-index property setter +static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure) +{ + if (!PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "z_index must be an integer"); + return -1; + } + + int val = PyLong_AsLong(value); + self->data->z_index = val; + + // Mark scene as needing resort + self->data->notifyZIndexChanged(); + + return 0; +} + +// Visible property getter (new for #87) +static PyObject* PyDrawable_get_visible(PyDrawableObject* self, void* closure) +{ + return PyBool_FromLong(self->data->visible); +} + +// Visible property setter (new for #87) +static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); + return -1; + } + + self->data->visible = (value == Py_True); + return 0; +} + +// Opacity property getter (new for #88) +static PyObject* PyDrawable_get_opacity(PyDrawableObject* self, void* closure) +{ + return PyFloat_FromDouble(self->data->opacity); +} + +// Opacity property setter (new for #88) +static int PyDrawable_set_opacity(PyDrawableObject* 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, "opacity must be a number"); + return -1; + } + + // Clamp to valid range + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + + self->data->opacity = val; + return 0; +} + +// GetSetDef array for properties +static PyGetSetDef PyDrawable_getsetters[] = { + {"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, + "Callable executed when object is clicked", NULL}, + {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, + "Z-order for rendering (lower values rendered first)", NULL}, + {"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible, + "Whether the object is visible", NULL}, + {"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity, + "Opacity level (0.0 = transparent, 1.0 = opaque)", NULL}, + {NULL} // Sentinel +}; + +// get_bounds method implementation (#89) +static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args)) +{ + auto bounds = self->data->get_bounds(); + return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); +} + +// move method implementation (#98) +static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args) +{ + float dx, dy; + if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { + return NULL; + } + + self->data->move(dx, dy); + Py_RETURN_NONE; +} + +// resize method implementation (#98) +static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) +{ + float w, h; + if (!PyArg_ParseTuple(args, "ff", &w, &h)) { + return NULL; + } + + self->data->resize(w, h); + Py_RETURN_NONE; +} + +// Method definitions +static PyMethodDef PyDrawable_methods[] = { + {"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS, + "Get bounding box as (x, y, width, height)"}, + {"move", (PyCFunction)PyDrawable_move, METH_VARARGS, + "Move by relative offset (dx, dy)"}, + {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, + "Resize to new dimensions (width, height)"}, + {NULL} // Sentinel +}; + +// Type initialization +static int PyDrawable_init(PyDrawableObject* self, PyObject* args, PyObject* kwds) +{ + PyErr_SetString(PyExc_TypeError, "_Drawable is an abstract base class and cannot be instantiated directly"); + return -1; +} + +namespace mcrfpydef { + PyTypeObject PyDrawableType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy._Drawable", + .tp_basicsize = sizeof(PyDrawableObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) { + PyDrawableObject* obj = (PyDrawableObject*)self; + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_flags = Py_TPFLAGS_DEFAULT, // | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR("Base class for all drawable UI elements"), + .tp_methods = PyDrawable_methods, + .tp_getset = PyDrawable_getsetters, + .tp_init = (initproc)PyDrawable_init, + .tp_new = PyType_GenericNew, + }; +} \ No newline at end of file diff --git a/src/PyDrawable.h b/src/PyDrawable.h new file mode 100644 index 0000000..7837a38 --- /dev/null +++ b/src/PyDrawable.h @@ -0,0 +1,15 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "UIDrawable.h" + +// Python object structure for UIDrawable base class +typedef struct { + PyObject_HEAD + std::shared_ptr data; +} PyDrawableObject; + +// Declare the Python type for _Drawable base class +namespace mcrfpydef { + extern PyTypeObject PyDrawableType; +} \ No newline at end of file diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 22b4787..2e954de 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -5,6 +5,17 @@ #include "PyFont.h" #include +UICaption::UICaption() +{ + // Initialize text with safe defaults + text.setString(""); + text.setPosition(0.0f, 0.0f); + text.setCharacterSize(12); + text.setFillColor(sf::Color::White); + text.setOutlineColor(sf::Color::Black); + text.setOutlineThickness(0.0f); +} + UIDrawable* UICaption::click_at(sf::Vector2f point) { if (click_callable) @@ -16,10 +27,22 @@ UIDrawable* UICaption::click_at(sf::Vector2f point) void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target) { + // Check visibility + if (!visible) return; + + // Apply opacity + auto color = text.getFillColor(); + color.a = static_cast(255 * opacity); + text.setFillColor(color); + text.move(offset); //Resources::game->getWindow().draw(text); target.draw(text); text.move(-offset); + + // Restore original alpha + color.a = 255; + text.setFillColor(color); } PyObjectsEnum UICaption::derived_type() @@ -27,6 +50,23 @@ PyObjectsEnum UICaption::derived_type() return PyObjectsEnum::UICAPTION; } +// Phase 1 implementations +sf::FloatRect UICaption::get_bounds() const +{ + return text.getGlobalBounds(); +} + +void UICaption::move(float dx, float dy) +{ + text.move(dx, dy); +} + +void UICaption::resize(float w, float h) +{ + // Caption doesn't support direct resizing - size is controlled by font size + // This is a no-op but required by the interface +} + PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure) { auto member_ptr = reinterpret_cast(closure); @@ -229,26 +269,31 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) //static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr }; //float x = 0.0f, y = 0.0f, outline = 0.0f; static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr }; - PyObject* pos; + PyObject* pos = NULL; float outline = 0.0f; - char* text; + char* text = NULL; PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL; //if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf", // const_cast(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline)) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf", + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOf", const_cast(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline)) { return -1; } - PyVectorObject* pos_result = PyVector::from_arg(pos); - if (!pos_result) - { - PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); - return -1; + // Handle position - default to (0, 0) if not provided + if (pos && pos != Py_None) { + PyVectorObject* pos_result = PyVector::from_arg(pos); + if (!pos_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + self->data->text.setPosition(pos_result->data); + } else { + self->data->text.setPosition(0.0f, 0.0f); } - self->data->text.setPosition(pos_result->data); // check types for font, fill_color, outline_color //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; @@ -275,7 +320,12 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) } } - self->data->text.setString((std::string)text); + // Handle text - default to empty string if not provided + if (text && text != NULL) { + self->data->text.setString((std::string)text); + } else { + self->data->text.setString(""); + } self->data->text.setOutlineThickness(outline); if (fill_color) { auto fc = PyColor::from_arg(fill_color); diff --git a/src/UICaption.h b/src/UICaption.h index 60d8e13..bd98489 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -7,10 +7,16 @@ class UICaption: public UIDrawable { public: sf::Text text; + UICaption(); // Default constructor with safe initialization void render(sf::Vector2f, sf::RenderTarget&) override final; PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; + // Property system for animations bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, const sf::Color& value) override; diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 4ff470f..2b6f9b9 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -51,6 +51,15 @@ public: // Notification for z_index changes void notifyZIndexChanged(); + // New properties for Phase 1 + bool visible = true; // #87 - visibility flag + float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque) + + // New virtual methods for Phase 1 + virtual sf::FloatRect get_bounds() const = 0; // #89 - get bounding box + virtual void move(float dx, float dy) = 0; // #98 - move by offset + virtual void resize(float w, float h) = 0; // #98 - resize to dimensions + // Animation support virtual bool setProperty(const std::string& name, float value) { return false; } virtual bool setProperty(const std::string& name, int value) { return false; } diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 41f10fa..172dded 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -5,7 +5,12 @@ #include "PyVector.h" -UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it +UIEntity::UIEntity() +: self(nullptr), grid(nullptr), position(0.0f, 0.0f), collision_pos(0, 0) +{ + // Initialize sprite with safe defaults (sprite has its own safe constructor now) + // gridstate vector starts empty since we don't know grid dimensions +} UIEntity::UIEntity(UIGrid& grid) : gridstate(grid.grid_x * grid.grid_y) @@ -67,25 +72,43 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { //static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr }; //float x = 0.0f, y = 0.0f, scale = 1.0f; static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr }; - PyObject* pos; + PyObject* pos = NULL; // Must initialize to NULL for optional arguments float scale = 1.0f; - int sprite_index = -1; + int sprite_index = 0; // Default to sprite index 0 instead of -1 PyObject* texture = NULL; PyObject* grid = NULL; //if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O", // const_cast(keywords), &x, &y, &texture, &sprite_index, &grid)) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO", + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiO", const_cast(keywords), &pos, &texture, &sprite_index, &grid)) { return -1; } - PyVectorObject* pos_result = PyVector::from_arg(pos); - if (!pos_result) - { - PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); - return -1; + // Handle position - default to (0, 0) if not provided + PyVectorObject* pos_result = nullptr; + if (pos && pos != Py_None) { + pos_result = PyVector::from_arg(pos); + if (!pos_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + } else { + // Create default position (0, 0) + PyObject* vector_class = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (vector_class) { + PyObject* pos_obj = PyObject_CallFunction(vector_class, "ff", 0.0f, 0.0f); + Py_DECREF(vector_class); + if (pos_obj) { + pos_result = (PyVectorObject*)pos_obj; + } + } + if (!pos_result) { + PyErr_SetString(PyExc_RuntimeError, "Failed to create default position vector"); + return -1; + } } // check types for texture @@ -104,10 +127,11 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - if (!texture_ptr) { - PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); - return -1; - } + // Allow creation without texture for testing purposes + // if (!texture_ptr) { + // PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); + // return -1; + // } if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); @@ -124,8 +148,19 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { Py_INCREF(self); // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers - self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); + if (texture_ptr) { + self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); + } else { + // Create an empty sprite for testing + self->data->sprite = UISprite(); + } self->data->position = pos_result->data; + + // Clean up the position object if we created it + if (!pos || pos == Py_None) { + Py_DECREF(pos_result); + } + if (grid != NULL) { PyUIGridObject* pygrid = (PyUIGridObject*)grid; self->data->grid = pygrid->data; @@ -244,6 +279,50 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl return 0; } +PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure) +{ + auto member_ptr = reinterpret_cast(closure); + if (member_ptr == 0) // x + return PyFloat_FromDouble(self->data->position.x); + else if (member_ptr == 1) // y + return PyFloat_FromDouble(self->data->position.y); + else + { + PyErr_SetString(PyExc_AttributeError, "Invalid attribute"); + return nullptr; + } +} + +int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure) +{ + float val; + auto member_ptr = reinterpret_cast(closure); + if (PyFloat_Check(value)) + { + val = PyFloat_AsDouble(value); + } + else if (PyLong_Check(value)) + { + val = PyLong_AsLong(value); + } + else + { + PyErr_SetString(PyExc_TypeError, "Value must be a floating point number."); + return -1; + } + if (member_ptr == 0) // x + { + self->data->position.x = val; + self->data->collision_pos.x = static_cast(val); + } + else if (member_ptr == 1) // y + { + self->data->position.y = val; + self->data->collision_pos.y = static_cast(val); + } + return 0; +} + PyMethodDef UIEntity::methods[] = { {"at", (PyCFunction)UIEntity::at, METH_O}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, @@ -256,6 +335,8 @@ PyGetSetDef UIEntity::getsetters[] = { {"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL}, {"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL}, {"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display (deprecated: use sprite_index)", NULL}, + {"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0}, + {"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1}, {NULL} /* Sentinel */ }; diff --git a/src/UIEntity.h b/src/UIEntity.h index 16f3d3d..9d605f2 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -60,6 +60,8 @@ public: static PyObject* get_gridstate(PyUIEntityObject* self, void* closure); static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure); static int set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure); + static PyObject* get_float_member(PyUIEntityObject* self, void* closure); + static int set_float_member(PyUIEntityObject* self, PyObject* value, void* closure); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; static PyObject* repr(PyUIEntityObject* self); diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index f6f7fa7..a784046 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -45,8 +45,31 @@ PyObjectsEnum UIFrame::derived_type() return PyObjectsEnum::UIFRAME; } +// Phase 1 implementations +sf::FloatRect UIFrame::get_bounds() const +{ + auto pos = box.getPosition(); + auto size = box.getSize(); + return sf::FloatRect(pos.x, pos.y, size.x, size.y); +} + +void UIFrame::move(float dx, float dy) +{ + box.move(dx, dy); +} + +void UIFrame::resize(float w, float h) +{ + box.setSize(sf::Vector2f(w, h)); +} + void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) { + // Check visibility + if (!visible) return; + + // TODO: Apply opacity when SFML supports it on shapes + box.move(offset); //Resources::game->getWindow().draw(box); target.draw(box); @@ -281,7 +304,7 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) PyObject* outline_color = 0; // First try to parse as (x, y, w, h, ...) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOf", const_cast(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline)) { PyErr_Clear(); // Clear the error @@ -289,20 +312,22 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) PyObject* pos_obj = nullptr; const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast(alt_keywords), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OffOOf", const_cast(alt_keywords), &pos_obj, &w, &h, &fill_color, &outline_color, &outline)) { return -1; } - // Convert position argument to x, y - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately"); - return -1; + // Convert position argument to x, y if provided + if (pos_obj && pos_obj != Py_None) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately"); + return -1; + } + x = vec->data.x; + y = vec->data.y; } - x = vec->data.x; - y = vec->data.y; } self->data->box.setPosition(sf::Vector2f(x, y)); diff --git a/src/UIFrame.h b/src/UIFrame.h index a296928..204482d 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -33,6 +33,11 @@ public: void move(sf::Vector2f); PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; + + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; static PyObject* get_children(PyUIFrameObject* self, void* closure); diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index e2ae8e5..2e03e03 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -3,7 +3,27 @@ #include "McRFPy_API.h" #include -UIGrid::UIGrid() {} +UIGrid::UIGrid() +: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr) +{ + // Initialize entities list + entities = std::make_shared>>(); + + // Initialize box with safe defaults + box.setSize(sf::Vector2f(0, 0)); + box.setPosition(sf::Vector2f(0, 0)); + box.setFillColor(sf::Color(0, 0, 0, 0)); + + // Initialize render texture (small default size) + renderTexture.create(1, 1); + + // Initialize output sprite + output.setTextureRect(sf::IntRect(0, 0, 0, 0)); + output.setPosition(0, 0); + output.setTexture(renderTexture.getTexture()); + + // Points vector starts empty (grid_x * grid_y = 0) +} UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _xy, sf::Vector2f _wh) : grid_x(gx), grid_y(gy), @@ -44,6 +64,11 @@ void UIGrid::update() {} void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) { + // Check visibility + if (!visible) return; + + // TODO: Apply opacity to output sprite + output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing // output size can change; update size when drawing output.setTextureRect( @@ -202,6 +227,29 @@ PyObjectsEnum UIGrid::derived_type() return PyObjectsEnum::UIGRID; } +// Phase 1 implementations +sf::FloatRect UIGrid::get_bounds() const +{ + auto pos = box.getPosition(); + auto size = box.getSize(); + return sf::FloatRect(pos.x, pos.y, size.x, size.y); +} + +void UIGrid::move(float dx, float dy) +{ + box.move(dx, dy); +} + +void UIGrid::resize(float w, float h) +{ + box.setSize(sf::Vector2f(w, h)); + // Recreate render texture with new size + if (w > 0 && h > 0) { + renderTexture.create(static_cast(w), static_cast(h)); + output.setTexture(renderTexture.getTexture()); + } +} + std::shared_ptr UIGrid::getTexture() { return ptex; @@ -218,14 +266,14 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - int grid_x, grid_y; + int grid_x = 0, grid_y = 0; // Default to 0x0 grid PyObject* textureObj = Py_None; //float box_x, box_y, box_w, box_h; PyObject* pos = NULL; PyObject* size = NULL; //if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) { - if (!PyArg_ParseTuple(args, "ii|OOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { + if (!PyArg_ParseTuple(args, "|iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { return -1; // If parsing fails, return an error } diff --git a/src/UIGrid.h b/src/UIGrid.h index 28aa174..ace9310 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -34,6 +34,11 @@ public: PyObjectsEnum derived_type() override final; //void setSprite(int); virtual UIDrawable* click_at(sf::Vector2f point) override final; + + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; int grid_x, grid_y; //int grid_size; // grid sizes are implied by IndexTexture now diff --git a/src/UISprite.cpp b/src/UISprite.cpp index e69d37e..90d0654 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -11,7 +11,13 @@ UIDrawable* UISprite::click_at(sf::Vector2f point) return NULL; } -UISprite::UISprite() {} +UISprite::UISprite() +: sprite_index(0), ptex(nullptr) +{ + // Initialize sprite to safe defaults + sprite.setPosition(0.0f, 0.0f); + sprite.setScale(1.0f, 1.0f); +} UISprite::UISprite(std::shared_ptr _ptex, int _sprite_index, sf::Vector2f _pos, float _scale) : ptex(_ptex), sprite_index(_sprite_index) @@ -30,9 +36,21 @@ void UISprite::render(sf::Vector2f offset) void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target) { + // Check visibility + if (!visible) return; + + // Apply opacity + auto color = sprite.getColor(); + color.a = static_cast(255 * opacity); + sprite.setColor(color); + sprite.move(offset); target.draw(sprite); sprite.move(-offset); + + // Restore original alpha + color.a = 255; + sprite.setColor(color); } void UISprite::setPosition(sf::Vector2f pos) @@ -84,6 +102,28 @@ PyObjectsEnum UISprite::derived_type() return PyObjectsEnum::UISPRITE; } +// Phase 1 implementations +sf::FloatRect UISprite::get_bounds() const +{ + return sprite.getGlobalBounds(); +} + +void UISprite::move(float dx, float dy) +{ + sprite.move(dx, dy); +} + +void UISprite::resize(float w, float h) +{ + // Calculate scale factors to achieve target size + auto bounds = sprite.getLocalBounds(); + if (bounds.width > 0 && bounds.height > 0) { + float scaleX = w / bounds.width; + float scaleY = h / bounds.height; + sprite.setScale(scaleX, scaleY); + } +} + PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure) { auto member_ptr = reinterpret_cast(closure); diff --git a/src/UISprite.h b/src/UISprite.h index 060b2c2..a036791 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -42,6 +42,11 @@ public: PyObjectsEnum derived_type() override final; + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; + // Property system for animations bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, int value) override;