feat: Grid camera defaults to tile (0,0) at top-left + center_camera() method (#169)
Changes: - Default Grid center now positions tile (0,0) at widget's top-left corner - Added center_camera() method to center grid's middle tile at view center - Added center_camera((tile_x, tile_y)) to position tile at top-left of widget - Uses NaN as sentinel to detect if user provided center values in kwargs - Animation-compatible: center_camera() just sets center property, no special state Behavior: - center_camera() → grid's center tile at view center - center_camera((0, 0)) → tile (0,0) at top-left corner - center_camera((5, 10)) → tile (5,10) at top-left corner Before: Grid(size=(320,240)) showed 3/4 of content off-screen (center=0,0) After: Grid(size=(320,240)) shows tile (0,0) at top-left (center=160,120) Closes #169 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
05f28ef7cd
commit
f62362032e
2 changed files with 106 additions and 3 deletions
103
src/UIGrid.cpp
103
src/UIGrid.cpp
|
|
@ -6,8 +6,9 @@
|
||||||
#include "Profiler.h"
|
#include "Profiler.h"
|
||||||
#include "PyFOV.h"
|
#include "PyFOV.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath> // #142 - for std::floor
|
#include <cmath> // #142 - for std::floor, std::isnan
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
#include <limits> // #169 - for std::numeric_limits
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIGrid::UIGrid()
|
UIGrid::UIGrid()
|
||||||
|
|
@ -735,7 +736,9 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
PyObject* fill_color = nullptr;
|
PyObject* fill_color = nullptr;
|
||||||
PyObject* click_handler = nullptr;
|
PyObject* click_handler = nullptr;
|
||||||
PyObject* layers_obj = nullptr; // #150 - layers dict
|
PyObject* layers_obj = nullptr; // #150 - layers dict
|
||||||
float center_x = 0.0f, center_y = 0.0f;
|
// #169 - Use NaN as sentinel to detect if user provided center values
|
||||||
|
float center_x = std::numeric_limits<float>::quiet_NaN();
|
||||||
|
float center_y = std::numeric_limits<float>::quiet_NaN();
|
||||||
float zoom = 1.0f;
|
float zoom = 1.0f;
|
||||||
// perspective is now handled via properties, not init args
|
// perspective is now handled via properties, not init args
|
||||||
int visible = 1;
|
int visible = 1;
|
||||||
|
|
@ -862,9 +865,19 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
||||||
|
|
||||||
// Set additional properties
|
// Set additional properties
|
||||||
|
self->data->zoom = zoom; // Set zoom first, needed for default center calculation
|
||||||
|
|
||||||
|
// #169 - Calculate default center if not provided by user
|
||||||
|
// Default: tile (0,0) at top-left of widget
|
||||||
|
if (std::isnan(center_x)) {
|
||||||
|
// Center = half widget size (in pixels), so tile 0,0 appears at top-left
|
||||||
|
center_x = w / (2.0f * zoom);
|
||||||
|
}
|
||||||
|
if (std::isnan(center_y)) {
|
||||||
|
center_y = h / (2.0f * zoom);
|
||||||
|
}
|
||||||
self->data->center_x = center_x;
|
self->data->center_x = center_x;
|
||||||
self->data->center_y = center_y;
|
self->data->center_y = center_y;
|
||||||
self->data->zoom = zoom;
|
|
||||||
// perspective is now handled by perspective_entity and perspective_enabled
|
// perspective is now handled by perspective_entity and perspective_enabled
|
||||||
// self->data->perspective = perspective;
|
// self->data->perspective = perspective;
|
||||||
self->data->visible = visible;
|
self->data->visible = visible;
|
||||||
|
|
@ -1730,6 +1743,72 @@ PyObject* UIGrid::py_entities_in_radius(PyUIGridObject* self, PyObject* args, Py
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #169 - center_camera implementations
|
||||||
|
void UIGrid::center_camera() {
|
||||||
|
// Center on grid's middle tile
|
||||||
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
center_x = (grid_x / 2.0f) * cell_width;
|
||||||
|
center_y = (grid_y / 2.0f) * cell_height;
|
||||||
|
markDirty(); // #144 - View change affects content
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGrid::center_camera(float tile_x, float tile_y) {
|
||||||
|
// Position specified tile at top-left of widget
|
||||||
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
// To put tile (tx, ty) at top-left: center = tile_pos + half_viewport
|
||||||
|
float half_viewport_x = box.getSize().x / zoom / 2.0f;
|
||||||
|
float half_viewport_y = box.getSize().y / zoom / 2.0f;
|
||||||
|
center_x = tile_x * cell_width + half_viewport_x;
|
||||||
|
center_y = tile_y * cell_height + half_viewport_y;
|
||||||
|
markDirty(); // #144 - View change affects content
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
|
||||||
|
PyObject* pos_arg = nullptr;
|
||||||
|
|
||||||
|
// Parse optional positional argument (tuple of tile coordinates)
|
||||||
|
if (!PyArg_ParseTuple(args, "|O", &pos_arg)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos_arg == nullptr || pos_arg == Py_None) {
|
||||||
|
// No args: center on grid's middle tile
|
||||||
|
self->data->center_camera();
|
||||||
|
} else if (PyTuple_Check(pos_arg) && PyTuple_Size(pos_arg) == 2) {
|
||||||
|
// Tuple provided: center on (tile_x, tile_y)
|
||||||
|
PyObject* x_obj = PyTuple_GetItem(pos_arg, 0);
|
||||||
|
PyObject* y_obj = PyTuple_GetItem(pos_arg, 1);
|
||||||
|
|
||||||
|
float tile_x, tile_y;
|
||||||
|
if (PyFloat_Check(x_obj)) {
|
||||||
|
tile_x = PyFloat_AsDouble(x_obj);
|
||||||
|
} else if (PyLong_Check(x_obj)) {
|
||||||
|
tile_x = (float)PyLong_AsLong(x_obj);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PyFloat_Check(y_obj)) {
|
||||||
|
tile_y = PyFloat_AsDouble(y_obj);
|
||||||
|
} else if (PyLong_Check(y_obj)) {
|
||||||
|
tile_y = (float)PyLong_AsLong(y_obj);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->center_camera(tile_x, tile_y);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "center_camera() takes an optional tuple (tile_x, tile_y)");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
PyMethodDef UIGrid::methods[] = {
|
PyMethodDef UIGrid::methods[] = {
|
||||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
|
@ -1818,6 +1897,15 @@ PyMethodDef UIGrid::methods[] = {
|
||||||
" radius: Search radius\n\n"
|
" radius: Search radius\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" List of Entity objects within the radius."},
|
" List of Entity objects within the radius."},
|
||||||
|
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
|
||||||
|
"center_camera(pos: tuple = None) -> None\n\n"
|
||||||
|
"Center the camera on a tile coordinate.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" grid.center_camera() # Center on middle of grid\n"
|
||||||
|
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
||||||
|
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
||||||
{NULL, NULL, 0, NULL}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1929,6 +2017,15 @@ PyMethodDef UIGrid_all_methods[] = {
|
||||||
" radius: Search radius\n\n"
|
" radius: Search radius\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" List of Entity objects within the radius."},
|
" List of Entity objects within the radius."},
|
||||||
|
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
|
||||||
|
"center_camera(pos: tuple = None) -> None\n\n"
|
||||||
|
"Center the camera on a tile coordinate.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" grid.center_camera() # Center on middle of grid\n"
|
||||||
|
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
||||||
|
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,12 @@ public:
|
||||||
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args);
|
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args);
|
||||||
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
||||||
|
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
||||||
|
|
||||||
|
// #169 - Camera positioning
|
||||||
|
void center_camera(); // Center on grid's middle tile
|
||||||
|
void center_camera(float tile_x, float tile_y); // Center on specific tile
|
||||||
|
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
static PyObject* get_entities(PyUIGridObject* self, void* closure);
|
static PyObject* get_entities(PyUIGridObject* self, void* closure);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue