2026-01-11 20:07:55 -05:00
|
|
|
#include "PyHeightMap.h"
|
|
|
|
|
#include "McRFPy_API.h"
|
|
|
|
|
#include "McRFPy_Doc.h"
|
|
|
|
|
#include <sstream>
|
|
|
|
|
|
|
|
|
|
// Property definitions
|
|
|
|
|
PyGetSetDef PyHeightMap::getsetters[] = {
|
|
|
|
|
{"size", (getter)PyHeightMap::get_size, NULL,
|
|
|
|
|
MCRF_PROPERTY(size, "Dimensions (width, height) of the heightmap. Read-only."), NULL},
|
|
|
|
|
{NULL}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Method definitions
|
|
|
|
|
PyMethodDef PyHeightMap::methods[] = {
|
|
|
|
|
{"fill", (PyCFunction)PyHeightMap::fill, METH_VARARGS,
|
|
|
|
|
MCRF_METHOD(HeightMap, fill,
|
|
|
|
|
MCRF_SIG("(value: float)", "HeightMap"),
|
|
|
|
|
MCRF_DESC("Set all cells to the specified value."),
|
|
|
|
|
MCRF_ARGS_START
|
|
|
|
|
MCRF_ARG("value", "The value to set for all cells")
|
|
|
|
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
|
|
|
)},
|
|
|
|
|
{"clear", (PyCFunction)PyHeightMap::clear, METH_NOARGS,
|
|
|
|
|
MCRF_METHOD(HeightMap, clear,
|
|
|
|
|
MCRF_SIG("()", "HeightMap"),
|
|
|
|
|
MCRF_DESC("Set all cells to 0.0. Equivalent to fill(0.0)."),
|
|
|
|
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
|
|
|
)},
|
|
|
|
|
{"add_constant", (PyCFunction)PyHeightMap::add_constant, METH_VARARGS,
|
|
|
|
|
MCRF_METHOD(HeightMap, add_constant,
|
|
|
|
|
MCRF_SIG("(value: float)", "HeightMap"),
|
|
|
|
|
MCRF_DESC("Add a constant value to every cell."),
|
|
|
|
|
MCRF_ARGS_START
|
|
|
|
|
MCRF_ARG("value", "The value to add to each cell")
|
|
|
|
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
|
|
|
)},
|
|
|
|
|
{"scale", (PyCFunction)PyHeightMap::scale, METH_VARARGS,
|
|
|
|
|
MCRF_METHOD(HeightMap, scale,
|
|
|
|
|
MCRF_SIG("(factor: float)", "HeightMap"),
|
|
|
|
|
MCRF_DESC("Multiply every cell by a factor."),
|
|
|
|
|
MCRF_ARGS_START
|
|
|
|
|
MCRF_ARG("factor", "The multiplier for each cell")
|
|
|
|
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
|
|
|
)},
|
|
|
|
|
{"clamp", (PyCFunction)PyHeightMap::clamp, METH_VARARGS | METH_KEYWORDS,
|
|
|
|
|
MCRF_METHOD(HeightMap, clamp,
|
|
|
|
|
MCRF_SIG("(min: float = 0.0, max: float = 1.0)", "HeightMap"),
|
|
|
|
|
MCRF_DESC("Clamp all values to the specified range."),
|
|
|
|
|
MCRF_ARGS_START
|
|
|
|
|
MCRF_ARG("min", "Minimum value (default 0.0)")
|
|
|
|
|
MCRF_ARG("max", "Maximum value (default 1.0)")
|
|
|
|
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
|
|
|
)},
|
|
|
|
|
{"normalize", (PyCFunction)PyHeightMap::normalize, METH_VARARGS | METH_KEYWORDS,
|
|
|
|
|
MCRF_METHOD(HeightMap, normalize,
|
|
|
|
|
MCRF_SIG("(min: float = 0.0, max: float = 1.0)", "HeightMap"),
|
|
|
|
|
MCRF_DESC("Linearly rescale values so the current minimum becomes min and current maximum becomes max."),
|
|
|
|
|
MCRF_ARGS_START
|
|
|
|
|
MCRF_ARG("min", "Target minimum value (default 0.0)")
|
|
|
|
|
MCRF_ARG("max", "Target maximum value (default 1.0)")
|
|
|
|
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
|
|
|
)},
|
2026-01-11 20:42:33 -05:00
|
|
|
// 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")
|
|
|
|
|
)},
|
2026-01-11 20:07:55 -05:00
|
|
|
{NULL}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Constructor
|
|
|
|
|
PyObject* PyHeightMap::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|
|
|
|
{
|
|
|
|
|
PyHeightMapObject* self = (PyHeightMapObject*)type->tp_alloc(type, 0);
|
|
|
|
|
if (self) {
|
|
|
|
|
self->heightmap = nullptr;
|
|
|
|
|
}
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int PyHeightMap::init(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
|
|
|
{
|
|
|
|
|
static const char* keywords[] = {"size", "fill", nullptr};
|
|
|
|
|
PyObject* size_obj = nullptr;
|
|
|
|
|
float fill_value = 0.0f;
|
|
|
|
|
|
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(keywords),
|
|
|
|
|
&size_obj, &fill_value)) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse size tuple
|
|
|
|
|
if (!PyTuple_Check(size_obj) || PyTuple_Size(size_obj) != 2) {
|
|
|
|
|
PyErr_SetString(PyExc_TypeError, "size must be a tuple of (width, height)");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int width = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
|
|
|
|
int height = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
|
|
|
|
|
|
|
|
|
if (PyErr_Occurred()) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
|
|
|
PyErr_SetString(PyExc_ValueError, "width and height must be positive integers");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:26:04 -05:00
|
|
|
if (width > GRID_MAX || height > GRID_MAX) {
|
|
|
|
|
PyErr_Format(PyExc_ValueError,
|
|
|
|
|
"HeightMap dimensions cannot exceed %d (got %dx%d)",
|
|
|
|
|
GRID_MAX, width, height);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:07:55 -05:00
|
|
|
// Clean up any existing heightmap
|
|
|
|
|
if (self->heightmap) {
|
|
|
|
|
TCOD_heightmap_delete(self->heightmap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new libtcod heightmap
|
|
|
|
|
self->heightmap = TCOD_heightmap_new(width, height);
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_MemoryError, "Failed to allocate heightmap");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fill with initial value if not zero
|
|
|
|
|
if (fill_value != 0.0f) {
|
|
|
|
|
// libtcod's TCOD_heightmap_add adds to all cells, so we use it after clear
|
|
|
|
|
TCOD_heightmap_clear(self->heightmap);
|
|
|
|
|
TCOD_heightmap_add(self->heightmap, fill_value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PyHeightMap::dealloc(PyHeightMapObject* self)
|
|
|
|
|
{
|
|
|
|
|
if (self->heightmap) {
|
|
|
|
|
TCOD_heightmap_delete(self->heightmap);
|
|
|
|
|
self->heightmap = nullptr;
|
|
|
|
|
}
|
|
|
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PyObject* PyHeightMap::repr(PyObject* obj)
|
|
|
|
|
{
|
|
|
|
|
PyHeightMapObject* self = (PyHeightMapObject*)obj;
|
|
|
|
|
std::ostringstream ss;
|
|
|
|
|
|
|
|
|
|
if (self->heightmap) {
|
|
|
|
|
ss << "<HeightMap (" << self->heightmap->w << " x " << self->heightmap->h << ")>";
|
|
|
|
|
} else {
|
|
|
|
|
ss << "<HeightMap (uninitialized)>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PyUnicode_FromString(ss.str().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Property: size
|
|
|
|
|
PyObject* PyHeightMap::get_size(PyHeightMapObject* self, void* closure)
|
|
|
|
|
{
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
return Py_BuildValue("(ii)", self->heightmap->w, self->heightmap->h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method: fill(value) -> HeightMap
|
|
|
|
|
PyObject* PyHeightMap::fill(PyHeightMapObject* self, PyObject* args)
|
|
|
|
|
{
|
|
|
|
|
float value;
|
|
|
|
|
if (!PyArg_ParseTuple(args, "f", &value)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear and then add the value (libtcod doesn't have a direct "set all" function)
|
|
|
|
|
TCOD_heightmap_clear(self->heightmap);
|
|
|
|
|
if (value != 0.0f) {
|
|
|
|
|
TCOD_heightmap_add(self->heightmap, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return self for chaining
|
|
|
|
|
Py_INCREF(self);
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method: clear() -> HeightMap
|
|
|
|
|
PyObject* PyHeightMap::clear(PyHeightMapObject* self, PyObject* Py_UNUSED(args))
|
|
|
|
|
{
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TCOD_heightmap_clear(self->heightmap);
|
|
|
|
|
|
|
|
|
|
// Return self for chaining
|
|
|
|
|
Py_INCREF(self);
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method: add_constant(value) -> HeightMap
|
|
|
|
|
PyObject* PyHeightMap::add_constant(PyHeightMapObject* self, PyObject* args)
|
|
|
|
|
{
|
|
|
|
|
float value;
|
|
|
|
|
if (!PyArg_ParseTuple(args, "f", &value)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TCOD_heightmap_add(self->heightmap, value);
|
|
|
|
|
|
|
|
|
|
// Return self for chaining
|
|
|
|
|
Py_INCREF(self);
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method: scale(factor) -> HeightMap
|
|
|
|
|
PyObject* PyHeightMap::scale(PyHeightMapObject* self, PyObject* args)
|
|
|
|
|
{
|
|
|
|
|
float factor;
|
|
|
|
|
if (!PyArg_ParseTuple(args, "f", &factor)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TCOD_heightmap_scale(self->heightmap, factor);
|
|
|
|
|
|
|
|
|
|
// Return self for chaining
|
|
|
|
|
Py_INCREF(self);
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method: clamp(min=0.0, max=1.0) -> HeightMap
|
|
|
|
|
PyObject* PyHeightMap::clamp(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
|
|
|
{
|
|
|
|
|
static const char* keywords[] = {"min", "max", nullptr};
|
|
|
|
|
float min_val = 0.0f;
|
|
|
|
|
float max_val = 1.0f;
|
|
|
|
|
|
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ff", const_cast<char**>(keywords),
|
|
|
|
|
&min_val, &max_val)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:26:04 -05:00
|
|
|
if (min_val > max_val) {
|
|
|
|
|
PyErr_SetString(PyExc_ValueError, "min must be less than or equal to max");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:07:55 -05:00
|
|
|
TCOD_heightmap_clamp(self->heightmap, min_val, max_val);
|
|
|
|
|
|
|
|
|
|
// Return self for chaining
|
|
|
|
|
Py_INCREF(self);
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method: normalize(min=0.0, max=1.0) -> HeightMap
|
|
|
|
|
PyObject* PyHeightMap::normalize(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
|
|
|
{
|
|
|
|
|
static const char* keywords[] = {"min", "max", nullptr};
|
|
|
|
|
float min_val = 0.0f;
|
|
|
|
|
float max_val = 1.0f;
|
|
|
|
|
|
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ff", const_cast<char**>(keywords),
|
|
|
|
|
&min_val, &max_val)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!self->heightmap) {
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:26:04 -05:00
|
|
|
if (min_val > max_val) {
|
|
|
|
|
PyErr_SetString(PyExc_ValueError, "min must be less than or equal to max");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:07:55 -05:00
|
|
|
TCOD_heightmap_normalize(self->heightmap, min_val, max_val);
|
|
|
|
|
|
|
|
|
|
// Return self for chaining
|
|
|
|
|
Py_INCREF(self);
|
|
|
|
|
return (PyObject*)self;
|
|
|
|
|
}
|
2026-01-11 20:42:33 -05:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|