Compare commits
No commits in common. "bf8557798acf53e960feaf9f80c99baa3e6d63ae" and "87444c2fd07993c43ee9b5cedd94f3c304a05029" have entirely different histories.
bf8557798a
...
87444c2fd0
6 changed files with 0 additions and 1064 deletions
|
|
@ -60,56 +60,6 @@ PyMethodDef PyHeightMap::methods[] = {
|
||||||
MCRF_ARG("max", "Target maximum value (default 1.0)")
|
MCRF_ARG("max", "Target maximum value (default 1.0)")
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||||
)},
|
)},
|
||||||
// Query methods (#196)
|
|
||||||
{"get", (PyCFunction)PyHeightMap::get, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, get,
|
|
||||||
MCRF_SIG("(pos: tuple[int, int])", "float"),
|
|
||||||
MCRF_DESC("Get the height value at integer coordinates."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("pos", "Position as (x, y) tuple")
|
|
||||||
MCRF_RETURNS("float: Height value at that position")
|
|
||||||
MCRF_RAISES("IndexError", "Position is out of bounds")
|
|
||||||
)},
|
|
||||||
{"get_interpolated", (PyCFunction)PyHeightMap::get_interpolated, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, get_interpolated,
|
|
||||||
MCRF_SIG("(pos: tuple[float, float])", "float"),
|
|
||||||
MCRF_DESC("Get interpolated height value at non-integer coordinates."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("pos", "Position as (x, y) tuple with float coordinates")
|
|
||||||
MCRF_RETURNS("float: Bilinearly interpolated height value")
|
|
||||||
)},
|
|
||||||
{"get_slope", (PyCFunction)PyHeightMap::get_slope, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, get_slope,
|
|
||||||
MCRF_SIG("(pos: tuple[int, int])", "float"),
|
|
||||||
MCRF_DESC("Get the slope at integer coordinates, from 0 (flat) to pi/2 (vertical)."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("pos", "Position as (x, y) tuple")
|
|
||||||
MCRF_RETURNS("float: Slope angle in radians (0 to pi/2)")
|
|
||||||
MCRF_RAISES("IndexError", "Position is out of bounds")
|
|
||||||
)},
|
|
||||||
{"get_normal", (PyCFunction)PyHeightMap::get_normal, METH_VARARGS | METH_KEYWORDS,
|
|
||||||
MCRF_METHOD(HeightMap, get_normal,
|
|
||||||
MCRF_SIG("(pos: tuple[float, float], water_level: float = 0.0)", "tuple[float, float, float]"),
|
|
||||||
MCRF_DESC("Get the normal vector at given coordinates for lighting calculations."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("pos", "Position as (x, y) tuple with float coordinates")
|
|
||||||
MCRF_ARG("water_level", "Water level below which terrain is considered flat (default 0.0)")
|
|
||||||
MCRF_RETURNS("tuple[float, float, float]: Normal vector (nx, ny, nz)")
|
|
||||||
)},
|
|
||||||
{"min_max", (PyCFunction)PyHeightMap::min_max, METH_NOARGS,
|
|
||||||
MCRF_METHOD(HeightMap, min_max,
|
|
||||||
MCRF_SIG("()", "tuple[float, float]"),
|
|
||||||
MCRF_DESC("Get the minimum and maximum height values in the map."),
|
|
||||||
MCRF_RETURNS("tuple[float, float]: (min_value, max_value)")
|
|
||||||
)},
|
|
||||||
{"count_in_range", (PyCFunction)PyHeightMap::count_in_range, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, count_in_range,
|
|
||||||
MCRF_SIG("(range: tuple[float, float])", "int"),
|
|
||||||
MCRF_DESC("Count cells with values in the specified range (inclusive)."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("range", "Value range as (min, max) tuple")
|
|
||||||
MCRF_RETURNS("int: Number of cells with values in range")
|
|
||||||
)},
|
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -350,191 +300,3 @@ PyObject* PyHeightMap::normalize(PyHeightMapObject* self, PyObject* args, PyObje
|
||||||
Py_INCREF(self);
|
Py_INCREF(self);
|
||||||
return (PyObject*)self;
|
return (PyObject*)self;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query methods (#196)
|
|
||||||
|
|
||||||
// Method: get(pos) -> float
|
|
||||||
PyObject* PyHeightMap::get(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
PyObject* pos_obj = nullptr;
|
|
||||||
if (!PyArg_ParseTuple(args, "O", &pos_obj)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse position tuple
|
|
||||||
if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int x = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
|
|
||||||
int y = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds check
|
|
||||||
if (x < 0 || x >= self->heightmap->w || y < 0 || y >= self->heightmap->h) {
|
|
||||||
PyErr_Format(PyExc_IndexError,
|
|
||||||
"Position (%d, %d) out of bounds for HeightMap of size (%d, %d)",
|
|
||||||
x, y, self->heightmap->w, self->heightmap->h);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float value = TCOD_heightmap_get_value(self->heightmap, x, y);
|
|
||||||
return PyFloat_FromDouble(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: get_interpolated(pos) -> float
|
|
||||||
PyObject* PyHeightMap::get_interpolated(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
PyObject* pos_obj = nullptr;
|
|
||||||
if (!PyArg_ParseTuple(args, "O", &pos_obj)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse position tuple (floats)
|
|
||||||
if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float x = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 0));
|
|
||||||
float y = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float value = TCOD_heightmap_get_interpolated_value(self->heightmap, x, y);
|
|
||||||
return PyFloat_FromDouble(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: get_slope(pos) -> float
|
|
||||||
PyObject* PyHeightMap::get_slope(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
PyObject* pos_obj = nullptr;
|
|
||||||
if (!PyArg_ParseTuple(args, "O", &pos_obj)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse position tuple
|
|
||||||
if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int x = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
|
|
||||||
int y = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds check
|
|
||||||
if (x < 0 || x >= self->heightmap->w || y < 0 || y >= self->heightmap->h) {
|
|
||||||
PyErr_Format(PyExc_IndexError,
|
|
||||||
"Position (%d, %d) out of bounds for HeightMap of size (%d, %d)",
|
|
||||||
x, y, self->heightmap->w, self->heightmap->h);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float slope = TCOD_heightmap_get_slope(self->heightmap, x, y);
|
|
||||||
return PyFloat_FromDouble(slope);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: get_normal(pos, water_level=0.0) -> tuple[float, float, float]
|
|
||||||
PyObject* PyHeightMap::get_normal(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
||||||
{
|
|
||||||
static const char* keywords[] = {"pos", "water_level", nullptr};
|
|
||||||
PyObject* pos_obj = nullptr;
|
|
||||||
float water_level = 0.0f;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(keywords),
|
|
||||||
&pos_obj, &water_level)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse position tuple (floats)
|
|
||||||
if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float x = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 0));
|
|
||||||
float y = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float n[3];
|
|
||||||
TCOD_heightmap_get_normal(self->heightmap, x, y, n, water_level);
|
|
||||||
|
|
||||||
return Py_BuildValue("(fff)", n[0], n[1], n[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: min_max() -> tuple[float, float]
|
|
||||||
PyObject* PyHeightMap::min_max(PyHeightMapObject* self, PyObject* Py_UNUSED(args))
|
|
||||||
{
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float min_val, max_val;
|
|
||||||
TCOD_heightmap_get_minmax(self->heightmap, &min_val, &max_val);
|
|
||||||
|
|
||||||
return Py_BuildValue("(ff)", min_val, max_val);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: count_in_range(range) -> int
|
|
||||||
PyObject* PyHeightMap::count_in_range(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
PyObject* range_obj = nullptr;
|
|
||||||
if (!PyArg_ParseTuple(args, "O", &range_obj)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse range tuple
|
|
||||||
if (!PyTuple_Check(range_obj) || PyTuple_Size(range_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "range must be a tuple of (min, max)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float min_val = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 0));
|
|
||||||
float max_val = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = TCOD_heightmap_count_cells(self->heightmap, min_val, max_val);
|
|
||||||
return PyLong_FromLong(count);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,6 @@ public:
|
||||||
static PyObject* clamp(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* clamp(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* normalize(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* normalize(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
// Query methods (#196)
|
|
||||||
static PyObject* get(PyHeightMapObject* self, PyObject* args);
|
|
||||||
static PyObject* get_interpolated(PyHeightMapObject* self, PyObject* args);
|
|
||||||
static PyObject* get_slope(PyHeightMapObject* self, PyObject* args);
|
|
||||||
static PyObject* get_normal(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
static PyObject* min_max(PyHeightMapObject* self, PyObject* Py_UNUSED(args));
|
|
||||||
static PyObject* count_in_range(PyHeightMapObject* self, PyObject* args);
|
|
||||||
|
|
||||||
// Method and property definitions
|
// Method and property definitions
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
|
|
|
||||||
282
src/UIGrid.cpp
282
src/UIGrid.cpp
|
|
@ -9,7 +9,6 @@
|
||||||
#include "PyFOV.h"
|
#include "PyFOV.h"
|
||||||
#include "PyPositionHelper.h" // For standardized position argument parsing
|
#include "PyPositionHelper.h" // For standardized position argument parsing
|
||||||
#include "PyVector.h" // #179, #181 - For Vector return types
|
#include "PyVector.h" // #179, #181 - For Vector return types
|
||||||
#include "PyHeightMap.h" // #199 - HeightMap application methods
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath> // #142 - for std::floor, std::isnan
|
#include <cmath> // #142 - for std::floor, std::isnan
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
|
@ -1691,229 +1690,6 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #199 - HeightMap application methods
|
|
||||||
|
|
||||||
PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|
||||||
static const char* keywords[] = {"source", "range", "walkable", "transparent", nullptr};
|
|
||||||
PyObject* source_obj = nullptr;
|
|
||||||
PyObject* range_obj = nullptr;
|
|
||||||
PyObject* walkable_obj = Py_None;
|
|
||||||
PyObject* transparent_obj = Py_None;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", const_cast<char**>(keywords),
|
|
||||||
&source_obj, &range_obj, &walkable_obj, &transparent_obj)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate source is a HeightMap
|
|
||||||
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
|
|
||||||
if (!heightmap_type) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
bool is_heightmap = PyObject_IsInstance(source_obj, heightmap_type);
|
|
||||||
Py_DECREF(heightmap_type);
|
|
||||||
if (!is_heightmap) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "source must be a HeightMap");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
PyHeightMapObject* hmap = (PyHeightMapObject*)source_obj;
|
|
||||||
|
|
||||||
if (!hmap->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse range tuple
|
|
||||||
if (!PyTuple_Check(range_obj) || PyTuple_Size(range_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "range must be a tuple of (min, max)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float range_min = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 0));
|
|
||||||
float range_max = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check size match
|
|
||||||
if (hmap->heightmap->w != self->data->grid_w || hmap->heightmap->h != self->data->grid_h) {
|
|
||||||
PyErr_Format(PyExc_ValueError,
|
|
||||||
"HeightMap size (%d, %d) does not match Grid size (%d, %d)",
|
|
||||||
hmap->heightmap->w, hmap->heightmap->h, self->data->grid_w, self->data->grid_h);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse optional walkable/transparent booleans
|
|
||||||
bool set_walkable = (walkable_obj != Py_None);
|
|
||||||
bool set_transparent = (transparent_obj != Py_None);
|
|
||||||
bool walkable_value = false;
|
|
||||||
bool transparent_value = false;
|
|
||||||
|
|
||||||
if (set_walkable) {
|
|
||||||
walkable_value = PyObject_IsTrue(walkable_obj);
|
|
||||||
}
|
|
||||||
if (set_transparent) {
|
|
||||||
transparent_value = PyObject_IsTrue(transparent_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply threshold
|
|
||||||
for (int y = 0; y < self->data->grid_h; y++) {
|
|
||||||
for (int x = 0; x < self->data->grid_w; x++) {
|
|
||||||
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
|
|
||||||
if (value >= range_min && value <= range_max) {
|
|
||||||
UIGridPoint& point = self->data->at(x, y);
|
|
||||||
if (set_walkable) {
|
|
||||||
point.walkable = walkable_value;
|
|
||||||
}
|
|
||||||
if (set_transparent) {
|
|
||||||
point.transparent = transparent_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync TCOD map if it exists
|
|
||||||
if (self->data->getTCODMap()) {
|
|
||||||
self->data->syncTCODMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* UIGrid::py_apply_ranges(PyUIGridObject* self, PyObject* args) {
|
|
||||||
PyObject* source_obj = nullptr;
|
|
||||||
PyObject* ranges_obj = nullptr;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "OO", &source_obj, &ranges_obj)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate source is a HeightMap
|
|
||||||
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
|
|
||||||
if (!heightmap_type) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
bool is_heightmap = PyObject_IsInstance(source_obj, heightmap_type);
|
|
||||||
Py_DECREF(heightmap_type);
|
|
||||||
if (!is_heightmap) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "source must be a HeightMap");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
PyHeightMapObject* hmap = (PyHeightMapObject*)source_obj;
|
|
||||||
|
|
||||||
if (!hmap->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate ranges is a list
|
|
||||||
if (!PyList_Check(ranges_obj)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "ranges must be a list");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check size match
|
|
||||||
if (hmap->heightmap->w != self->data->grid_w || hmap->heightmap->h != self->data->grid_h) {
|
|
||||||
PyErr_Format(PyExc_ValueError,
|
|
||||||
"HeightMap size (%d, %d) does not match Grid size (%d, %d)",
|
|
||||||
hmap->heightmap->w, hmap->heightmap->h, self->data->grid_w, self->data->grid_h);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse all ranges first to catch errors early
|
|
||||||
struct RangeEntry {
|
|
||||||
float min, max;
|
|
||||||
bool set_walkable, set_transparent;
|
|
||||||
bool walkable_value, transparent_value;
|
|
||||||
};
|
|
||||||
std::vector<RangeEntry> entries;
|
|
||||||
|
|
||||||
Py_ssize_t num_ranges = PyList_Size(ranges_obj);
|
|
||||||
for (Py_ssize_t i = 0; i < num_ranges; i++) {
|
|
||||||
PyObject* entry = PyList_GetItem(ranges_obj, i);
|
|
||||||
|
|
||||||
if (!PyTuple_Check(entry) || PyTuple_Size(entry) != 2) {
|
|
||||||
PyErr_Format(PyExc_TypeError,
|
|
||||||
"ranges[%zd] must be a tuple of (range, properties_dict)", i);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* range_tuple = PyTuple_GetItem(entry, 0);
|
|
||||||
PyObject* props_dict = PyTuple_GetItem(entry, 1);
|
|
||||||
|
|
||||||
if (!PyTuple_Check(range_tuple) || PyTuple_Size(range_tuple) != 2) {
|
|
||||||
PyErr_Format(PyExc_TypeError,
|
|
||||||
"ranges[%zd] range must be a tuple of (min, max)", i);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PyDict_Check(props_dict)) {
|
|
||||||
PyErr_Format(PyExc_TypeError,
|
|
||||||
"ranges[%zd] properties must be a dict", i);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
RangeEntry re;
|
|
||||||
re.min = (float)PyFloat_AsDouble(PyTuple_GetItem(range_tuple, 0));
|
|
||||||
re.max = (float)PyFloat_AsDouble(PyTuple_GetItem(range_tuple, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse walkable from dict
|
|
||||||
PyObject* walkable_val = PyDict_GetItemString(props_dict, "walkable");
|
|
||||||
re.set_walkable = (walkable_val != nullptr);
|
|
||||||
if (re.set_walkable) {
|
|
||||||
re.walkable_value = PyObject_IsTrue(walkable_val);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse transparent from dict
|
|
||||||
PyObject* transparent_val = PyDict_GetItemString(props_dict, "transparent");
|
|
||||||
re.set_transparent = (transparent_val != nullptr);
|
|
||||||
if (re.set_transparent) {
|
|
||||||
re.transparent_value = PyObject_IsTrue(transparent_val);
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push_back(re);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply all ranges in a single pass
|
|
||||||
for (int y = 0; y < self->data->grid_h; y++) {
|
|
||||||
for (int x = 0; x < self->data->grid_w; x++) {
|
|
||||||
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
|
|
||||||
UIGridPoint& point = self->data->at(x, y);
|
|
||||||
|
|
||||||
// Check each range (first match wins)
|
|
||||||
for (const auto& re : entries) {
|
|
||||||
if (value >= re.min && value <= re.max) {
|
|
||||||
if (re.set_walkable) {
|
|
||||||
point.walkable = re.walkable_value;
|
|
||||||
}
|
|
||||||
if (re.set_transparent) {
|
|
||||||
point.transparent = re.transparent_value;
|
|
||||||
}
|
|
||||||
break; // First matching range wins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync TCOD map if it exists
|
|
||||||
if (self->data->getTCODMap()) {
|
|
||||||
self->data->syncTCODMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -1983,35 +1759,6 @@ PyMethodDef UIGrid::methods[] = {
|
||||||
" grid.center_camera() # Center on middle of grid\n"
|
" grid.center_camera() # Center on middle of grid\n"
|
||||||
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
||||||
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
||||||
// #199 - HeightMap application methods
|
|
||||||
{"apply_threshold", (PyCFunction)UIGrid::py_apply_threshold, METH_VARARGS | METH_KEYWORDS,
|
|
||||||
"apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> Grid\n\n"
|
|
||||||
"Apply walkable/transparent properties where heightmap values are in range.\n\n"
|
|
||||||
"Args:\n"
|
|
||||||
" source: HeightMap with values to check. Must match grid size.\n"
|
|
||||||
" range: Tuple of (min, max) - cells with values in this range are affected.\n"
|
|
||||||
" walkable: If not None, set walkable to this value for cells in range.\n"
|
|
||||||
" transparent: If not None, set transparent to this value for cells in range.\n\n"
|
|
||||||
"Returns:\n"
|
|
||||||
" Grid: self, for method chaining.\n\n"
|
|
||||||
"Raises:\n"
|
|
||||||
" ValueError: If HeightMap size doesn't match grid size."},
|
|
||||||
{"apply_ranges", (PyCFunction)UIGrid::py_apply_ranges, METH_VARARGS,
|
|
||||||
"apply_ranges(source: HeightMap, ranges: list) -> Grid\n\n"
|
|
||||||
"Apply multiple thresholds in a single pass.\n\n"
|
|
||||||
"Args:\n"
|
|
||||||
" source: HeightMap with values to check. Must match grid size.\n"
|
|
||||||
" ranges: List of (range_tuple, properties_dict) tuples.\n"
|
|
||||||
" range_tuple: (min, max) value range\n"
|
|
||||||
" properties_dict: {'walkable': bool, 'transparent': bool}\n\n"
|
|
||||||
"Returns:\n"
|
|
||||||
" Grid: self, for method chaining.\n\n"
|
|
||||||
"Example:\n"
|
|
||||||
" grid.apply_ranges(terrain, [\n"
|
|
||||||
" ((0.0, 0.3), {'walkable': False, 'transparent': True}), # Water\n"
|
|
||||||
" ((0.3, 0.8), {'walkable': True, 'transparent': True}), # Land\n"
|
|
||||||
" ((0.8, 1.0), {'walkable': False, 'transparent': False}), # Mountains\n"
|
|
||||||
" ])"},
|
|
||||||
{NULL, NULL, 0, NULL}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2104,35 +1851,6 @@ PyMethodDef UIGrid_all_methods[] = {
|
||||||
" grid.center_camera() # Center on middle of grid\n"
|
" grid.center_camera() # Center on middle of grid\n"
|
||||||
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
||||||
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
||||||
// #199 - HeightMap application methods
|
|
||||||
{"apply_threshold", (PyCFunction)UIGrid::py_apply_threshold, METH_VARARGS | METH_KEYWORDS,
|
|
||||||
"apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> Grid\n\n"
|
|
||||||
"Apply walkable/transparent properties where heightmap values are in range.\n\n"
|
|
||||||
"Args:\n"
|
|
||||||
" source: HeightMap with values to check. Must match grid size.\n"
|
|
||||||
" range: Tuple of (min, max) - cells with values in this range are affected.\n"
|
|
||||||
" walkable: If not None, set walkable to this value for cells in range.\n"
|
|
||||||
" transparent: If not None, set transparent to this value for cells in range.\n\n"
|
|
||||||
"Returns:\n"
|
|
||||||
" Grid: self, for method chaining.\n\n"
|
|
||||||
"Raises:\n"
|
|
||||||
" ValueError: If HeightMap size doesn't match grid size."},
|
|
||||||
{"apply_ranges", (PyCFunction)UIGrid::py_apply_ranges, METH_VARARGS,
|
|
||||||
"apply_ranges(source: HeightMap, ranges: list) -> Grid\n\n"
|
|
||||||
"Apply multiple thresholds in a single pass.\n\n"
|
|
||||||
"Args:\n"
|
|
||||||
" source: HeightMap with values to check. Must match grid size.\n"
|
|
||||||
" ranges: List of (range_tuple, properties_dict) tuples.\n"
|
|
||||||
" range_tuple: (min, max) value range\n"
|
|
||||||
" properties_dict: {'walkable': bool, 'transparent': bool}\n\n"
|
|
||||||
"Returns:\n"
|
|
||||||
" Grid: self, for method chaining.\n\n"
|
|
||||||
"Example:\n"
|
|
||||||
" grid.apply_ranges(terrain, [\n"
|
|
||||||
" ((0.0, 0.3), {'walkable': False, 'transparent': True}), # Water\n"
|
|
||||||
" ((0.3, 0.8), {'walkable': True, 'transparent': True}), # Land\n"
|
|
||||||
" ((0.8, 1.0), {'walkable': False, 'transparent': False}), # Mountains\n"
|
|
||||||
" ])"},
|
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,10 +179,6 @@ public:
|
||||||
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
|
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
||||||
|
|
||||||
// #199 - HeightMap application methods
|
|
||||||
static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
static PyObject* py_apply_ranges(PyUIGridObject* self, PyObject* args);
|
|
||||||
|
|
||||||
// #169 - Camera positioning
|
// #169 - Camera positioning
|
||||||
void center_camera(); // Center on grid's middle tile
|
void center_camera(); // Center on grid's middle tile
|
||||||
void center_camera(float tile_x, float tile_y); // Center on specific tile
|
void center_camera(float tile_x, float tile_y); // Center on specific tile
|
||||||
|
|
|
||||||
|
|
@ -1,310 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Unit tests for Grid.apply_threshold and Grid.apply_ranges (#199)
|
|
||||||
|
|
||||||
Tests the Grid methods for applying HeightMap data to walkable/transparent properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_walkable():
|
|
||||||
"""apply_threshold sets walkable property correctly"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
# All cells start with default walkable
|
|
||||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True)
|
|
||||||
|
|
||||||
# Check a few cells
|
|
||||||
assert grid.at((5, 5)).walkable == True
|
|
||||||
assert grid.at((0, 0)).walkable == True
|
|
||||||
assert grid.at((9, 9)).walkable == True
|
|
||||||
print("PASS: test_apply_threshold_walkable")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_transparent():
|
|
||||||
"""apply_threshold sets transparent property correctly"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
grid.apply_threshold(hmap, range=(0.0, 1.0), transparent=False)
|
|
||||||
|
|
||||||
assert grid.at((5, 5)).transparent == False
|
|
||||||
print("PASS: test_apply_threshold_transparent")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_both():
|
|
||||||
"""apply_threshold sets both walkable and transparent"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True, transparent=True)
|
|
||||||
|
|
||||||
point = grid.at((5, 5))
|
|
||||||
assert point.walkable == True
|
|
||||||
assert point.transparent == True
|
|
||||||
print("PASS: test_apply_threshold_both")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_out_of_range():
|
|
||||||
"""apply_threshold doesn't affect cells outside range"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
# Set initial state
|
|
||||||
grid.at((5, 5)).walkable = False
|
|
||||||
grid.at((5, 5)).transparent = False
|
|
||||||
|
|
||||||
# Apply threshold with range that excludes 0.5
|
|
||||||
grid.apply_threshold(hmap, range=(0.0, 0.4), walkable=True, transparent=True)
|
|
||||||
|
|
||||||
# Cell should remain unchanged
|
|
||||||
assert grid.at((5, 5)).walkable == False
|
|
||||||
assert grid.at((5, 5)).transparent == False
|
|
||||||
print("PASS: test_apply_threshold_out_of_range")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_returns_self():
|
|
||||||
"""apply_threshold returns self for chaining"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
result = grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True)
|
|
||||||
assert result is grid, "apply_threshold should return self"
|
|
||||||
print("PASS: test_apply_threshold_returns_self")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_size_mismatch():
|
|
||||||
"""apply_threshold raises ValueError for size mismatch"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((20, 20), fill=0.5)
|
|
||||||
|
|
||||||
try:
|
|
||||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True)
|
|
||||||
print("FAIL: test_apply_threshold_size_mismatch - should have raised ValueError")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
assert "size" in str(e).lower()
|
|
||||||
|
|
||||||
print("PASS: test_apply_threshold_size_mismatch")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_invalid_source():
|
|
||||||
"""apply_threshold raises TypeError for non-HeightMap source"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
|
|
||||||
try:
|
|
||||||
grid.apply_threshold("not a heightmap", range=(0.0, 1.0), walkable=True)
|
|
||||||
print("FAIL: test_apply_threshold_invalid_source - should have raised TypeError")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_apply_threshold_invalid_source")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_threshold_none_values():
|
|
||||||
"""apply_threshold with None values leaves properties unchanged"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
# Set initial state
|
|
||||||
grid.at((5, 5)).walkable = True
|
|
||||||
grid.at((5, 5)).transparent = False
|
|
||||||
|
|
||||||
# Apply with only walkable=False, transparent should stay unchanged
|
|
||||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=False)
|
|
||||||
|
|
||||||
assert grid.at((5, 5)).walkable == False
|
|
||||||
assert grid.at((5, 5)).transparent == False # Unchanged
|
|
||||||
print("PASS: test_apply_threshold_none_values")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_basic():
|
|
||||||
"""apply_ranges applies multiple ranges correctly"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
# Apply a range that covers 0.5
|
|
||||||
grid.apply_ranges(hmap, [
|
|
||||||
((0.4, 0.6), {"walkable": True, "transparent": True}),
|
|
||||||
])
|
|
||||||
|
|
||||||
assert grid.at((5, 5)).walkable == True
|
|
||||||
assert grid.at((5, 5)).transparent == True
|
|
||||||
print("PASS: test_apply_ranges_basic")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_first_match_wins():
|
|
||||||
"""apply_ranges uses first matching range"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
# Both ranges cover 0.5, first should win
|
|
||||||
grid.apply_ranges(hmap, [
|
|
||||||
((0.0, 0.6), {"walkable": True}),
|
|
||||||
((0.4, 1.0), {"walkable": False}),
|
|
||||||
])
|
|
||||||
|
|
||||||
assert grid.at((5, 5)).walkable == True # First match wins
|
|
||||||
print("PASS: test_apply_ranges_first_match_wins")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_returns_self():
|
|
||||||
"""apply_ranges returns self for chaining"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
result = grid.apply_ranges(hmap, [
|
|
||||||
((0.0, 1.0), {"walkable": True}),
|
|
||||||
])
|
|
||||||
assert result is grid, "apply_ranges should return self"
|
|
||||||
print("PASS: test_apply_ranges_returns_self")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_size_mismatch():
|
|
||||||
"""apply_ranges raises ValueError for size mismatch"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((5, 5), fill=0.5)
|
|
||||||
|
|
||||||
try:
|
|
||||||
grid.apply_ranges(hmap, [
|
|
||||||
((0.0, 1.0), {"walkable": True}),
|
|
||||||
])
|
|
||||||
print("FAIL: test_apply_ranges_size_mismatch - should have raised ValueError")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
assert "size" in str(e).lower()
|
|
||||||
|
|
||||||
print("PASS: test_apply_ranges_size_mismatch")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_empty_list():
|
|
||||||
"""apply_ranges with empty list doesn't change anything"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
grid.at((5, 5)).walkable = True
|
|
||||||
grid.at((5, 5)).transparent = False
|
|
||||||
|
|
||||||
grid.apply_ranges(hmap, [])
|
|
||||||
|
|
||||||
# Should remain unchanged
|
|
||||||
assert grid.at((5, 5)).walkable == True
|
|
||||||
assert grid.at((5, 5)).transparent == False
|
|
||||||
print("PASS: test_apply_ranges_empty_list")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_no_match():
|
|
||||||
"""apply_ranges leaves cells unchanged when no range matches"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
grid.at((5, 5)).walkable = True
|
|
||||||
grid.at((5, 5)).transparent = True
|
|
||||||
|
|
||||||
# Ranges that don't include 0.5
|
|
||||||
grid.apply_ranges(hmap, [
|
|
||||||
((0.0, 0.4), {"walkable": False}),
|
|
||||||
((0.6, 1.0), {"transparent": False}),
|
|
||||||
])
|
|
||||||
|
|
||||||
# Should remain unchanged
|
|
||||||
assert grid.at((5, 5)).walkable == True
|
|
||||||
assert grid.at((5, 5)).transparent == True
|
|
||||||
print("PASS: test_apply_ranges_no_match")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_ranges_invalid_format():
|
|
||||||
"""apply_ranges raises TypeError for invalid format"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
|
|
||||||
# Invalid: not a list
|
|
||||||
try:
|
|
||||||
grid.apply_ranges(hmap, "not a list")
|
|
||||||
print("FAIL: should have raised TypeError for non-list")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Invalid: entry not a tuple
|
|
||||||
try:
|
|
||||||
grid.apply_ranges(hmap, ["not a tuple"])
|
|
||||||
print("FAIL: should have raised TypeError for non-tuple entry")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Invalid: range not a tuple
|
|
||||||
try:
|
|
||||||
grid.apply_ranges(hmap, [
|
|
||||||
([0.0, 1.0], {"walkable": True}), # list instead of tuple for range
|
|
||||||
])
|
|
||||||
print("FAIL: should have raised TypeError for non-tuple range")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Invalid: props not a dict
|
|
||||||
try:
|
|
||||||
grid.apply_ranges(hmap, [
|
|
||||||
((0.0, 1.0), "not a dict"),
|
|
||||||
])
|
|
||||||
print("FAIL: should have raised TypeError for non-dict props")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_apply_ranges_invalid_format")
|
|
||||||
|
|
||||||
|
|
||||||
def test_chaining():
|
|
||||||
"""Methods can be chained together"""
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
|
|
||||||
# Chain multiple operations
|
|
||||||
hmap.fill(0.5)
|
|
||||||
|
|
||||||
result = (grid
|
|
||||||
.apply_threshold(hmap, range=(0.0, 0.4), walkable=False)
|
|
||||||
.apply_threshold(hmap, range=(0.6, 1.0), transparent=False)
|
|
||||||
.apply_ranges(hmap, [
|
|
||||||
((0.4, 0.6), {"walkable": True, "transparent": True}),
|
|
||||||
]))
|
|
||||||
|
|
||||||
assert result is grid
|
|
||||||
print("PASS: test_chaining")
|
|
||||||
|
|
||||||
|
|
||||||
def run_all_tests():
|
|
||||||
"""Run all tests"""
|
|
||||||
print("Running Grid apply method tests...")
|
|
||||||
print()
|
|
||||||
|
|
||||||
test_apply_threshold_walkable()
|
|
||||||
test_apply_threshold_transparent()
|
|
||||||
test_apply_threshold_both()
|
|
||||||
test_apply_threshold_out_of_range()
|
|
||||||
test_apply_threshold_returns_self()
|
|
||||||
test_apply_threshold_size_mismatch()
|
|
||||||
test_apply_threshold_invalid_source()
|
|
||||||
test_apply_threshold_none_values()
|
|
||||||
test_apply_ranges_basic()
|
|
||||||
test_apply_ranges_first_match_wins()
|
|
||||||
test_apply_ranges_returns_self()
|
|
||||||
test_apply_ranges_size_mismatch()
|
|
||||||
test_apply_ranges_empty_list()
|
|
||||||
test_apply_ranges_no_match()
|
|
||||||
test_apply_ranges_invalid_format()
|
|
||||||
test_chaining()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("All Grid apply method tests PASSED!")
|
|
||||||
|
|
||||||
|
|
||||||
# Run tests directly
|
|
||||||
run_all_tests()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Unit tests for mcrfpy.HeightMap query methods (#196)
|
|
||||||
|
|
||||||
Tests the HeightMap query methods: get, get_interpolated, get_slope, get_normal, min_max, count_in_range
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import math
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_basic():
|
|
||||||
"""get() returns correct value at position"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
value = hmap.get((5, 5))
|
|
||||||
assert abs(value - 0.5) < 0.001, f"Expected 0.5, got {value}"
|
|
||||||
print("PASS: test_get_basic")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_corners():
|
|
||||||
"""get() works at all corners"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.25)
|
|
||||||
|
|
||||||
# All corners should have the fill value
|
|
||||||
assert abs(hmap.get((0, 0)) - 0.25) < 0.001
|
|
||||||
assert abs(hmap.get((9, 0)) - 0.25) < 0.001
|
|
||||||
assert abs(hmap.get((0, 9)) - 0.25) < 0.001
|
|
||||||
assert abs(hmap.get((9, 9)) - 0.25) < 0.001
|
|
||||||
print("PASS: test_get_corners")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_out_of_bounds():
|
|
||||||
"""get() raises IndexError for out-of-bounds position"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
|
|
||||||
# Test various out-of-bounds positions
|
|
||||||
for pos in [(-1, 0), (0, -1), (10, 0), (0, 10), (10, 10)]:
|
|
||||||
try:
|
|
||||||
hmap.get(pos)
|
|
||||||
print(f"FAIL: test_get_out_of_bounds - should have raised IndexError for {pos}")
|
|
||||||
sys.exit(1)
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_get_out_of_bounds")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_invalid_type():
|
|
||||||
"""get() raises TypeError for invalid position"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
|
|
||||||
try:
|
|
||||||
hmap.get([5, 5]) # list instead of tuple
|
|
||||||
print("FAIL: test_get_invalid_type - should have raised TypeError")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_get_invalid_type")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_interpolated_basic():
|
|
||||||
"""get_interpolated() returns value at float position"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
value = hmap.get_interpolated((5.5, 5.5))
|
|
||||||
# With uniform fill, interpolation should return same value
|
|
||||||
assert abs(value - 0.5) < 0.001, f"Expected ~0.5, got {value}"
|
|
||||||
print("PASS: test_get_interpolated_basic")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_interpolated_at_integers():
|
|
||||||
"""get_interpolated() matches get() at integer positions"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.75)
|
|
||||||
|
|
||||||
int_value = hmap.get((3, 4))
|
|
||||||
interp_value = hmap.get_interpolated((3.0, 4.0))
|
|
||||||
|
|
||||||
assert abs(int_value - interp_value) < 0.001, f"Values differ: {int_value} vs {interp_value}"
|
|
||||||
print("PASS: test_get_interpolated_at_integers")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_slope_flat():
|
|
||||||
"""get_slope() returns 0 for flat terrain"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
slope = hmap.get_slope((5, 5))
|
|
||||||
# Flat terrain should have slope near 0
|
|
||||||
assert abs(slope) < 0.01, f"Expected ~0 for flat terrain, got {slope}"
|
|
||||||
print("PASS: test_get_slope_flat")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_slope_out_of_bounds():
|
|
||||||
"""get_slope() raises IndexError for out-of-bounds position"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
|
|
||||||
try:
|
|
||||||
hmap.get_slope((10, 5))
|
|
||||||
print("FAIL: test_get_slope_out_of_bounds - should have raised IndexError")
|
|
||||||
sys.exit(1)
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_get_slope_out_of_bounds")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_normal_flat():
|
|
||||||
"""get_normal() returns up vector for flat terrain"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
nx, ny, nz = hmap.get_normal((5.0, 5.0))
|
|
||||||
|
|
||||||
# Flat terrain should have normal pointing up (0, 0, 1)
|
|
||||||
assert abs(nx) < 0.01, f"Expected nx~0, got {nx}"
|
|
||||||
assert abs(ny) < 0.01, f"Expected ny~0, got {ny}"
|
|
||||||
assert abs(nz - 1.0) < 0.01, f"Expected nz~1, got {nz}"
|
|
||||||
print("PASS: test_get_normal_flat")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_normal_with_water_level():
|
|
||||||
"""get_normal() accepts water_level parameter"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
nx, ny, nz = hmap.get_normal((5.0, 5.0), water_level=0.3)
|
|
||||||
|
|
||||||
# Should still return valid normal
|
|
||||||
assert isinstance(nx, float)
|
|
||||||
assert isinstance(ny, float)
|
|
||||||
assert isinstance(nz, float)
|
|
||||||
print("PASS: test_get_normal_with_water_level")
|
|
||||||
|
|
||||||
|
|
||||||
def test_min_max_uniform():
|
|
||||||
"""min_max() returns correct values for uniform heightmap"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
min_val, max_val = hmap.min_max()
|
|
||||||
|
|
||||||
assert abs(min_val - 0.5) < 0.001, f"Expected min=0.5, got {min_val}"
|
|
||||||
assert abs(max_val - 0.5) < 0.001, f"Expected max=0.5, got {max_val}"
|
|
||||||
print("PASS: test_min_max_uniform")
|
|
||||||
|
|
||||||
|
|
||||||
def test_min_max_after_operations():
|
|
||||||
"""min_max() updates after operations"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
hmap.fill(0.0).add_constant(0.5).scale(2.0)
|
|
||||||
|
|
||||||
min_val, max_val = hmap.min_max()
|
|
||||||
expected = 1.0 # 0.0 + 0.5 * 2.0
|
|
||||||
|
|
||||||
assert abs(min_val - expected) < 0.001, f"Expected min={expected}, got {min_val}"
|
|
||||||
assert abs(max_val - expected) < 0.001, f"Expected max={expected}, got {max_val}"
|
|
||||||
print("PASS: test_min_max_after_operations")
|
|
||||||
|
|
||||||
|
|
||||||
def test_count_in_range_all():
|
|
||||||
"""count_in_range() returns all cells for uniform map in range"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
count = hmap.count_in_range((0.0, 1.0))
|
|
||||||
|
|
||||||
assert count == 100, f"Expected 100 cells, got {count}"
|
|
||||||
print("PASS: test_count_in_range_all")
|
|
||||||
|
|
||||||
|
|
||||||
def test_count_in_range_none():
|
|
||||||
"""count_in_range() returns 0 when no cells in range"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
count = hmap.count_in_range((0.0, 0.4))
|
|
||||||
|
|
||||||
assert count == 0, f"Expected 0 cells, got {count}"
|
|
||||||
print("PASS: test_count_in_range_none")
|
|
||||||
|
|
||||||
|
|
||||||
def test_count_in_range_exact():
|
|
||||||
"""count_in_range() with exact bounds"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
count = hmap.count_in_range((0.5, 0.5))
|
|
||||||
|
|
||||||
# Should count all cells since fill value is exactly 0.5
|
|
||||||
assert count == 100, f"Expected 100 cells at exact value, got {count}"
|
|
||||||
print("PASS: test_count_in_range_exact")
|
|
||||||
|
|
||||||
|
|
||||||
def test_count_in_range_invalid():
|
|
||||||
"""count_in_range() raises TypeError for invalid range"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
|
|
||||||
try:
|
|
||||||
hmap.count_in_range([0.0, 1.0]) # list instead of tuple
|
|
||||||
print("FAIL: test_count_in_range_invalid - should have raised TypeError")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_count_in_range_invalid")
|
|
||||||
|
|
||||||
|
|
||||||
def run_all_tests():
|
|
||||||
"""Run all tests"""
|
|
||||||
print("Running HeightMap query method tests...")
|
|
||||||
print()
|
|
||||||
|
|
||||||
test_get_basic()
|
|
||||||
test_get_corners()
|
|
||||||
test_get_out_of_bounds()
|
|
||||||
test_get_invalid_type()
|
|
||||||
test_get_interpolated_basic()
|
|
||||||
test_get_interpolated_at_integers()
|
|
||||||
test_get_slope_flat()
|
|
||||||
test_get_slope_out_of_bounds()
|
|
||||||
test_get_normal_flat()
|
|
||||||
test_get_normal_with_water_level()
|
|
||||||
test_min_max_uniform()
|
|
||||||
test_min_max_after_operations()
|
|
||||||
test_count_in_range_all()
|
|
||||||
test_count_in_range_none()
|
|
||||||
test_count_in_range_exact()
|
|
||||||
test_count_in_range_invalid()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("All HeightMap query method tests PASSED!")
|
|
||||||
|
|
||||||
|
|
||||||
# Run tests directly
|
|
||||||
run_all_tests()
|
|
||||||
sys.exit(0)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue