parent
6caf3dcd05
commit
e5d0eb4847
5 changed files with 1422 additions and 0 deletions
|
|
@ -23,6 +23,7 @@
|
|||
#include "UIGridPathfinding.h" // AStarPath and DijkstraMap types
|
||||
#include "PyHeightMap.h" // Procedural generation heightmap (#193)
|
||||
#include "PyBSP.h" // Procedural generation BSP (#202-206)
|
||||
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
|
||||
#include "McRogueFaceVersion.h"
|
||||
#include "GameEngine.h"
|
||||
#include "ImGuiConsole.h"
|
||||
|
|
@ -420,6 +421,7 @@ PyObject* PyInit_mcrfpy()
|
|||
/*procedural generation (#192)*/
|
||||
&mcrfpydef::PyHeightMapType,
|
||||
&mcrfpydef::PyBSPType,
|
||||
&mcrfpydef::PyNoiseSourceType,
|
||||
|
||||
nullptr};
|
||||
|
||||
|
|
@ -460,6 +462,10 @@ PyObject* PyInit_mcrfpy()
|
|||
mcrfpydef::PyBSPNodeType.tp_methods = PyBSPNode::methods;
|
||||
mcrfpydef::PyBSPNodeType.tp_getset = PyBSPNode::getsetters;
|
||||
|
||||
// Set up PyNoiseSourceType methods and getsetters (#207-208)
|
||||
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
|
||||
mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters;
|
||||
|
||||
// Set up weakref support for all types that need it
|
||||
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
||||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyPositionHelper.h" // Standardized position argument parsing
|
||||
#include "PyNoiseSource.h" // For direct noise sampling (#209)
|
||||
#include "PyBSP.h" // For direct BSP sampling (#209)
|
||||
#include <sstream>
|
||||
#include <cstdlib> // For random seed handling
|
||||
#include <ctime> // For time-based seeds
|
||||
#include <vector> // For BSP node collection
|
||||
|
||||
// Property definitions
|
||||
PyGetSetDef PyHeightMap::getsetters[] = {
|
||||
|
|
@ -221,6 +224,126 @@ PyMethodDef PyHeightMap::methods[] = {
|
|||
MCRF_ARG("iterations", "Number of smoothing passes (default 1)")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
)},
|
||||
// Combination operations (#194)
|
||||
{"add", (PyCFunction)PyHeightMap::add, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, add,
|
||||
MCRF_SIG("(other: HeightMap)", "HeightMap"),
|
||||
MCRF_DESC("Add another heightmap's values to this one cell-by-cell."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions to add")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
{"subtract", (PyCFunction)PyHeightMap::subtract, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, subtract,
|
||||
MCRF_SIG("(other: HeightMap)", "HeightMap"),
|
||||
MCRF_DESC("Subtract another heightmap's values from this one cell-by-cell."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions to subtract")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
{"multiply", (PyCFunction)PyHeightMap::multiply, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, multiply,
|
||||
MCRF_SIG("(other: HeightMap)", "HeightMap"),
|
||||
MCRF_DESC("Multiply this heightmap by another cell-by-cell (useful for masking)."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions to multiply by")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
{"lerp", (PyCFunction)PyHeightMap::lerp, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, lerp,
|
||||
MCRF_SIG("(other: HeightMap, t: float)", "HeightMap"),
|
||||
MCRF_DESC("Linear interpolation between this and another heightmap."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions to interpolate towards")
|
||||
MCRF_ARG("t", "Interpolation factor (0.0 = this, 1.0 = other)")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
{"copy_from", (PyCFunction)PyHeightMap::copy_from, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, copy_from,
|
||||
MCRF_SIG("(other: HeightMap)", "HeightMap"),
|
||||
MCRF_DESC("Copy all values from another heightmap."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions to copy from")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
{"max", (PyCFunction)PyHeightMap::hmap_max, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, max,
|
||||
MCRF_SIG("(other: HeightMap)", "HeightMap"),
|
||||
MCRF_DESC("Set each cell to the maximum of this and another heightmap."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
{"min", (PyCFunction)PyHeightMap::hmap_min, METH_VARARGS,
|
||||
MCRF_METHOD(HeightMap, min,
|
||||
MCRF_SIG("(other: HeightMap)", "HeightMap"),
|
||||
MCRF_DESC("Set each cell to the minimum of this and another heightmap."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "HeightMap with same dimensions")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "Dimensions don't match")
|
||||
)},
|
||||
// Direct source sampling (#209)
|
||||
{"add_noise", (PyCFunction)PyHeightMap::add_noise, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(HeightMap, add_noise,
|
||||
MCRF_SIG("(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, "
|
||||
"mode: str = 'fbm', octaves: int = 4, scale: float = 1.0)", "HeightMap"),
|
||||
MCRF_DESC("Sample noise and add to current values. More efficient than creating intermediate HeightMap."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("source", "2D NoiseSource to sample from")
|
||||
MCRF_ARG("world_origin", "World coordinates of top-left (default: (0, 0))")
|
||||
MCRF_ARG("world_size", "World area to sample (default: HeightMap size)")
|
||||
MCRF_ARG("mode", "'flat', 'fbm', or 'turbulence' (default: 'fbm')")
|
||||
MCRF_ARG("octaves", "Octaves for fbm/turbulence (default: 4)")
|
||||
MCRF_ARG("scale", "Multiplier for sampled values (default: 1.0)")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
)},
|
||||
{"multiply_noise", (PyCFunction)PyHeightMap::multiply_noise, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(HeightMap, multiply_noise,
|
||||
MCRF_SIG("(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, "
|
||||
"mode: str = 'fbm', octaves: int = 4, scale: float = 1.0)", "HeightMap"),
|
||||
MCRF_DESC("Sample noise and multiply with current values. Useful for applying noise-based masks."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("source", "2D NoiseSource to sample from")
|
||||
MCRF_ARG("world_origin", "World coordinates of top-left (default: (0, 0))")
|
||||
MCRF_ARG("world_size", "World area to sample (default: HeightMap size)")
|
||||
MCRF_ARG("mode", "'flat', 'fbm', or 'turbulence' (default: 'fbm')")
|
||||
MCRF_ARG("octaves", "Octaves for fbm/turbulence (default: 4)")
|
||||
MCRF_ARG("scale", "Multiplier for sampled values (default: 1.0)")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
)},
|
||||
{"add_bsp", (PyCFunction)PyHeightMap::add_bsp, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(HeightMap, add_bsp,
|
||||
MCRF_SIG("(bsp: BSP, select: str = 'leaves', nodes: list = None, "
|
||||
"shrink: int = 0, value: float = 1.0)", "HeightMap"),
|
||||
MCRF_DESC("Add BSP node regions to heightmap. More efficient than creating intermediate HeightMap."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("bsp", "BSP tree to sample from")
|
||||
MCRF_ARG("select", "'leaves', 'all', or 'internal' (default: 'leaves')")
|
||||
MCRF_ARG("nodes", "Override: specific BSPNodes only (default: None)")
|
||||
MCRF_ARG("shrink", "Pixels to shrink from node bounds (default: 0)")
|
||||
MCRF_ARG("value", "Value to add inside regions (default: 1.0)")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
)},
|
||||
{"multiply_bsp", (PyCFunction)PyHeightMap::multiply_bsp, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(HeightMap, multiply_bsp,
|
||||
MCRF_SIG("(bsp: BSP, select: str = 'leaves', nodes: list = None, "
|
||||
"shrink: int = 0, value: float = 1.0)", "HeightMap"),
|
||||
MCRF_DESC("Multiply by BSP regions. Effectively masks the heightmap to node interiors."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("bsp", "BSP tree to sample from")
|
||||
MCRF_ARG("select", "'leaves', 'all', or 'internal' (default: 'leaves')")
|
||||
MCRF_ARG("nodes", "Override: specific BSPNodes only (default: None)")
|
||||
MCRF_ARG("shrink", "Pixels to shrink from node bounds (default: 0)")
|
||||
MCRF_ARG("value", "Value to multiply inside regions (default: 1.0)")
|
||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -1174,3 +1297,672 @@ PyObject* PyHeightMap::smooth(PyHeightMapObject* self, PyObject* args, PyObject*
|
|||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combination operations (#194)
|
||||
// =============================================================================
|
||||
|
||||
// Helper: Validate other HeightMap and check dimensions match
|
||||
static PyHeightMapObject* validateOtherHeightMap(PyHeightMapObject* self, PyObject* args, const char* method_name)
|
||||
{
|
||||
PyObject* other_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &other_obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check that other 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;
|
||||
}
|
||||
|
||||
int is_hmap = PyObject_IsInstance(other_obj, heightmap_type);
|
||||
Py_DECREF(heightmap_type);
|
||||
|
||||
if (is_hmap < 0) {
|
||||
return nullptr; // Error in isinstance check
|
||||
}
|
||||
if (!is_hmap) {
|
||||
PyErr_Format(PyExc_TypeError, "%s() requires a HeightMap argument", method_name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyHeightMapObject* other = (PyHeightMapObject*)other_obj;
|
||||
|
||||
// Check both are initialized
|
||||
if (!self->heightmap) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
if (!other->heightmap) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Other HeightMap not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check dimensions match
|
||||
if (self->heightmap->w != other->heightmap->w ||
|
||||
self->heightmap->h != other->heightmap->h) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"%s() requires HeightMaps with same dimensions: self is (%d, %d), other is (%d, %d)",
|
||||
method_name, self->heightmap->w, self->heightmap->h,
|
||||
other->heightmap->w, other->heightmap->h);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return other;
|
||||
}
|
||||
|
||||
// Method: add(other) -> HeightMap
|
||||
PyObject* PyHeightMap::add(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, args, "add");
|
||||
if (!other) return nullptr;
|
||||
|
||||
// TCOD_heightmap_add_hm adds hm1 + hm2 into out
|
||||
// We want self = self + other, so we can use self as out
|
||||
TCOD_heightmap_add_hm(self->heightmap, other->heightmap, self->heightmap);
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: subtract(other) -> HeightMap
|
||||
PyObject* PyHeightMap::subtract(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, args, "subtract");
|
||||
if (!other) return nullptr;
|
||||
|
||||
// No direct TCOD function - do cell-by-cell
|
||||
for (int y = 0; y < self->heightmap->h; y++) {
|
||||
for (int x = 0; x < self->heightmap->w; x++) {
|
||||
float v1 = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
float v2 = TCOD_heightmap_get_value(other->heightmap, x, y);
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, v1 - v2);
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: multiply(other) -> HeightMap
|
||||
PyObject* PyHeightMap::multiply(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, args, "multiply");
|
||||
if (!other) return nullptr;
|
||||
|
||||
// TCOD_heightmap_multiply_hm multiplies hm1 * hm2 into out
|
||||
TCOD_heightmap_multiply_hm(self->heightmap, other->heightmap, self->heightmap);
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: lerp(other, t) -> HeightMap
|
||||
PyObject* PyHeightMap::lerp(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyObject* other_obj;
|
||||
float t;
|
||||
if (!PyArg_ParseTuple(args, "Of", &other_obj, &t)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create args tuple with just the other object for validation
|
||||
PyObject* other_args = PyTuple_Pack(1, other_obj);
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, other_args, "lerp");
|
||||
Py_DECREF(other_args);
|
||||
if (!other) return nullptr;
|
||||
|
||||
// TCOD_heightmap_lerp_hm lerps hm1 and hm2 into out with coef
|
||||
// When coef=0, out=hm1. When coef=1, out=hm2
|
||||
TCOD_heightmap_lerp_hm(self->heightmap, other->heightmap, self->heightmap, t);
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: copy_from(other) -> HeightMap
|
||||
PyObject* PyHeightMap::copy_from(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, args, "copy_from");
|
||||
if (!other) return nullptr;
|
||||
|
||||
// TCOD_heightmap_copy copies source to dest
|
||||
TCOD_heightmap_copy(other->heightmap, self->heightmap);
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: max(other) -> HeightMap
|
||||
PyObject* PyHeightMap::hmap_max(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, args, "max");
|
||||
if (!other) return nullptr;
|
||||
|
||||
// No direct TCOD function - do cell-by-cell
|
||||
for (int y = 0; y < self->heightmap->h; y++) {
|
||||
for (int x = 0; x < self->heightmap->w; x++) {
|
||||
float v1 = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
float v2 = TCOD_heightmap_get_value(other->heightmap, x, y);
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, v1 > v2 ? v1 : v2);
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: min(other) -> HeightMap
|
||||
PyObject* PyHeightMap::hmap_min(PyHeightMapObject* self, PyObject* args)
|
||||
{
|
||||
PyHeightMapObject* other = validateOtherHeightMap(self, args, "min");
|
||||
if (!other) return nullptr;
|
||||
|
||||
// No direct TCOD function - do cell-by-cell
|
||||
for (int y = 0; y < self->heightmap->h; y++) {
|
||||
for (int x = 0; x < self->heightmap->w; x++) {
|
||||
float v1 = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
float v2 = TCOD_heightmap_get_value(other->heightmap, x, y);
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, v1 < v2 ? v1 : v2);
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Direct source sampling (#209)
|
||||
// =============================================================================
|
||||
|
||||
// Enum for noise sampling mode
|
||||
enum class NoiseSampleMode { FLAT, FBM, TURBULENCE };
|
||||
|
||||
// Helper: Parse noise sampling parameters
|
||||
static bool parseNoiseSampleParams(
|
||||
PyObject* args, PyObject* kwds,
|
||||
PyNoiseSourceObject** out_source,
|
||||
float* out_origin_x, float* out_origin_y,
|
||||
float* out_world_w, float* out_world_h,
|
||||
NoiseSampleMode* out_mode,
|
||||
int* out_octaves,
|
||||
float* out_scale,
|
||||
int hmap_w, int hmap_h,
|
||||
const char* method_name)
|
||||
{
|
||||
static const char* keywords[] = {"source", "world_origin", "world_size", "mode", "octaves", "scale", nullptr};
|
||||
PyObject* source_obj = nullptr;
|
||||
PyObject* origin_obj = nullptr;
|
||||
PyObject* world_size_obj = nullptr;
|
||||
const char* mode_str = "fbm";
|
||||
int octaves = 4;
|
||||
float scale = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOsif", const_cast<char**>(keywords),
|
||||
&source_obj, &origin_obj, &world_size_obj, &mode_str, &octaves, &scale)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate source is a NoiseSource
|
||||
PyObject* noise_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "NoiseSource");
|
||||
if (!noise_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "NoiseSource type not found in module");
|
||||
return false;
|
||||
}
|
||||
int is_noise = PyObject_IsInstance(source_obj, noise_type);
|
||||
Py_DECREF(noise_type);
|
||||
|
||||
if (is_noise < 0) return false;
|
||||
if (!is_noise) {
|
||||
PyErr_Format(PyExc_TypeError, "%s() requires a NoiseSource argument", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
PyNoiseSourceObject* source = (PyNoiseSourceObject*)source_obj;
|
||||
|
||||
// Check NoiseSource is 2D
|
||||
if (source->dimensions != 2) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"%s() requires a 2D NoiseSource, but source has %d dimensions",
|
||||
method_name, source->dimensions);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check NoiseSource is initialized
|
||||
if (!source->noise) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "NoiseSource not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse world_origin (default: (0, 0))
|
||||
float origin_x = 0.0f, origin_y = 0.0f;
|
||||
if (origin_obj && origin_obj != Py_None) {
|
||||
if (!PyTuple_Check(origin_obj) || PyTuple_Size(origin_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "world_origin must be a tuple of (x, y)");
|
||||
return false;
|
||||
}
|
||||
PyObject* ox = PyTuple_GetItem(origin_obj, 0);
|
||||
PyObject* oy = PyTuple_GetItem(origin_obj, 1);
|
||||
if (PyFloat_Check(ox)) origin_x = (float)PyFloat_AsDouble(ox);
|
||||
else if (PyLong_Check(ox)) origin_x = (float)PyLong_AsLong(ox);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_origin values must be numeric"); return false; }
|
||||
if (PyFloat_Check(oy)) origin_y = (float)PyFloat_AsDouble(oy);
|
||||
else if (PyLong_Check(oy)) origin_y = (float)PyLong_AsLong(oy);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_origin values must be numeric"); return false; }
|
||||
}
|
||||
|
||||
// Parse world_size (default: HeightMap size)
|
||||
float world_w = (float)hmap_w, world_h = (float)hmap_h;
|
||||
if (world_size_obj && world_size_obj != Py_None) {
|
||||
if (!PyTuple_Check(world_size_obj) || PyTuple_Size(world_size_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "world_size must be a tuple of (width, height)");
|
||||
return false;
|
||||
}
|
||||
PyObject* ww = PyTuple_GetItem(world_size_obj, 0);
|
||||
PyObject* wh = PyTuple_GetItem(world_size_obj, 1);
|
||||
if (PyFloat_Check(ww)) world_w = (float)PyFloat_AsDouble(ww);
|
||||
else if (PyLong_Check(ww)) world_w = (float)PyLong_AsLong(ww);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_size values must be numeric"); return false; }
|
||||
if (PyFloat_Check(wh)) world_h = (float)PyFloat_AsDouble(wh);
|
||||
else if (PyLong_Check(wh)) world_h = (float)PyLong_AsLong(wh);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_size values must be numeric"); return false; }
|
||||
}
|
||||
|
||||
// Parse mode
|
||||
NoiseSampleMode mode;
|
||||
if (strcmp(mode_str, "flat") == 0) {
|
||||
mode = NoiseSampleMode::FLAT;
|
||||
} else if (strcmp(mode_str, "fbm") == 0) {
|
||||
mode = NoiseSampleMode::FBM;
|
||||
} else if (strcmp(mode_str, "turbulence") == 0) {
|
||||
mode = NoiseSampleMode::TURBULENCE;
|
||||
} else {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"mode must be 'flat', 'fbm', or 'turbulence', got '%s'",
|
||||
mode_str);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate octaves
|
||||
if (octaves < 1 || octaves > TCOD_NOISE_MAX_OCTAVES) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"octaves must be between 1 and %d, got %d",
|
||||
TCOD_NOISE_MAX_OCTAVES, octaves);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set outputs
|
||||
*out_source = source;
|
||||
*out_origin_x = origin_x;
|
||||
*out_origin_y = origin_y;
|
||||
*out_world_w = world_w;
|
||||
*out_world_h = world_h;
|
||||
*out_mode = mode;
|
||||
*out_octaves = octaves;
|
||||
*out_scale = scale;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method: add_noise(source, ...) -> HeightMap
|
||||
PyObject* PyHeightMap::add_noise(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->heightmap) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyNoiseSourceObject* source;
|
||||
float origin_x, origin_y, world_w, world_h, scale;
|
||||
NoiseSampleMode mode;
|
||||
int octaves;
|
||||
|
||||
if (!parseNoiseSampleParams(args, kwds, &source,
|
||||
&origin_x, &origin_y, &world_w, &world_h,
|
||||
&mode, &octaves, &scale,
|
||||
self->heightmap->w, self->heightmap->h, "add_noise")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Sample noise and add to heightmap
|
||||
float coords[2];
|
||||
for (int y = 0; y < self->heightmap->h; y++) {
|
||||
for (int x = 0; x < self->heightmap->w; x++) {
|
||||
coords[0] = origin_x + ((float)x / (float)self->heightmap->w) * world_w;
|
||||
coords[1] = origin_y + ((float)y / (float)self->heightmap->h) * world_h;
|
||||
|
||||
float noise_value;
|
||||
switch (mode) {
|
||||
case NoiseSampleMode::FLAT:
|
||||
noise_value = TCOD_noise_get(source->noise, coords);
|
||||
break;
|
||||
case NoiseSampleMode::FBM:
|
||||
noise_value = TCOD_noise_get_fbm(source->noise, coords, (float)octaves);
|
||||
break;
|
||||
case NoiseSampleMode::TURBULENCE:
|
||||
noise_value = TCOD_noise_get_turbulence(source->noise, coords, (float)octaves);
|
||||
break;
|
||||
}
|
||||
|
||||
float current = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, current + noise_value * scale);
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: multiply_noise(source, ...) -> HeightMap
|
||||
PyObject* PyHeightMap::multiply_noise(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->heightmap) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyNoiseSourceObject* source;
|
||||
float origin_x, origin_y, world_w, world_h, scale;
|
||||
NoiseSampleMode mode;
|
||||
int octaves;
|
||||
|
||||
if (!parseNoiseSampleParams(args, kwds, &source,
|
||||
&origin_x, &origin_y, &world_w, &world_h,
|
||||
&mode, &octaves, &scale,
|
||||
self->heightmap->w, self->heightmap->h, "multiply_noise")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Sample noise and multiply with heightmap
|
||||
float coords[2];
|
||||
for (int y = 0; y < self->heightmap->h; y++) {
|
||||
for (int x = 0; x < self->heightmap->w; x++) {
|
||||
coords[0] = origin_x + ((float)x / (float)self->heightmap->w) * world_w;
|
||||
coords[1] = origin_y + ((float)y / (float)self->heightmap->h) * world_h;
|
||||
|
||||
float noise_value;
|
||||
switch (mode) {
|
||||
case NoiseSampleMode::FLAT:
|
||||
noise_value = TCOD_noise_get(source->noise, coords);
|
||||
break;
|
||||
case NoiseSampleMode::FBM:
|
||||
noise_value = TCOD_noise_get_fbm(source->noise, coords, (float)octaves);
|
||||
break;
|
||||
case NoiseSampleMode::TURBULENCE:
|
||||
noise_value = TCOD_noise_get_turbulence(source->noise, coords, (float)octaves);
|
||||
break;
|
||||
}
|
||||
|
||||
float current = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, current * (noise_value * scale));
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Helper: Collect BSP nodes based on select mode
|
||||
static bool collectBSPNodes(
|
||||
PyBSPObject* bsp,
|
||||
const char* select_str,
|
||||
PyObject* nodes_list,
|
||||
std::vector<TCOD_bsp_t*>& out_nodes,
|
||||
const char* method_name)
|
||||
{
|
||||
// If nodes list provided, use it directly
|
||||
if (nodes_list && nodes_list != Py_None) {
|
||||
if (!PyList_Check(nodes_list)) {
|
||||
PyErr_Format(PyExc_TypeError, "%s() nodes must be a list of BSPNode", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* bspnode_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "BSPNode");
|
||||
if (!bspnode_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSPNode type not found in module");
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t count = PyList_Size(nodes_list);
|
||||
for (Py_ssize_t i = 0; i < count; i++) {
|
||||
PyObject* item = PyList_GetItem(nodes_list, i);
|
||||
int is_node = PyObject_IsInstance(item, bspnode_type);
|
||||
if (is_node < 0) {
|
||||
Py_DECREF(bspnode_type);
|
||||
return false;
|
||||
}
|
||||
if (!is_node) {
|
||||
Py_DECREF(bspnode_type);
|
||||
PyErr_Format(PyExc_TypeError, "%s() nodes[%zd] is not a BSPNode", method_name, i);
|
||||
return false;
|
||||
}
|
||||
|
||||
PyBSPNodeObject* node = (PyBSPNodeObject*)item;
|
||||
if (!PyBSPNode::checkValid(node)) {
|
||||
Py_DECREF(bspnode_type);
|
||||
return false; // Error already set
|
||||
}
|
||||
out_nodes.push_back(node->node);
|
||||
}
|
||||
Py_DECREF(bspnode_type);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Determine selection mode
|
||||
enum class SelectMode { LEAVES, ALL, INTERNAL };
|
||||
SelectMode select;
|
||||
if (strcmp(select_str, "leaves") == 0) {
|
||||
select = SelectMode::LEAVES;
|
||||
} else if (strcmp(select_str, "all") == 0) {
|
||||
select = SelectMode::ALL;
|
||||
} else if (strcmp(select_str, "internal") == 0) {
|
||||
select = SelectMode::INTERNAL;
|
||||
} else {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"%s() select must be 'leaves', 'all', or 'internal', got '%s'",
|
||||
method_name, select_str);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect nodes from BSP tree
|
||||
// Use post-order traversal to collect all nodes
|
||||
std::vector<TCOD_bsp_t*> stack;
|
||||
stack.push_back(bsp->root);
|
||||
|
||||
while (!stack.empty()) {
|
||||
TCOD_bsp_t* node = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
bool is_leaf = TCOD_bsp_is_leaf(node);
|
||||
bool include = false;
|
||||
|
||||
switch (select) {
|
||||
case SelectMode::LEAVES:
|
||||
include = is_leaf;
|
||||
break;
|
||||
case SelectMode::ALL:
|
||||
include = true;
|
||||
break;
|
||||
case SelectMode::INTERNAL:
|
||||
include = !is_leaf;
|
||||
break;
|
||||
}
|
||||
|
||||
if (include) {
|
||||
out_nodes.push_back(node);
|
||||
}
|
||||
|
||||
// Add children (if any) using libtcod functions
|
||||
TCOD_bsp_t* left = TCOD_bsp_left(node);
|
||||
TCOD_bsp_t* right = TCOD_bsp_right(node);
|
||||
if (left) stack.push_back(left);
|
||||
if (right) stack.push_back(right);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method: add_bsp(bsp, ...) -> HeightMap
|
||||
PyObject* PyHeightMap::add_bsp(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->heightmap) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* keywords[] = {"bsp", "select", "nodes", "shrink", "value", nullptr};
|
||||
PyObject* bsp_obj = nullptr;
|
||||
const char* select_str = "leaves";
|
||||
PyObject* nodes_obj = nullptr;
|
||||
int shrink = 0;
|
||||
float value = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|sOif", const_cast<char**>(keywords),
|
||||
&bsp_obj, &select_str, &nodes_obj, &shrink, &value)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Validate bsp is a BSP
|
||||
PyObject* bsp_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "BSP");
|
||||
if (!bsp_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP type not found in module");
|
||||
return nullptr;
|
||||
}
|
||||
int is_bsp = PyObject_IsInstance(bsp_obj, bsp_type);
|
||||
Py_DECREF(bsp_type);
|
||||
|
||||
if (is_bsp < 0) return nullptr;
|
||||
if (!is_bsp) {
|
||||
PyErr_SetString(PyExc_TypeError, "add_bsp() requires a BSP argument");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyBSPObject* bsp = (PyBSPObject*)bsp_obj;
|
||||
if (!bsp->root) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Collect nodes
|
||||
std::vector<TCOD_bsp_t*> nodes;
|
||||
if (!collectBSPNodes(bsp, select_str, nodes_obj, nodes, "add_bsp")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Add value to each node's region
|
||||
for (TCOD_bsp_t* node : nodes) {
|
||||
int x1 = node->x + shrink;
|
||||
int y1 = node->y + shrink;
|
||||
int x2 = node->x + node->w - shrink;
|
||||
int y2 = node->y + node->h - shrink;
|
||||
|
||||
// Clamp to heightmap bounds and skip if shrunk to nothing
|
||||
if (x1 >= x2 || y1 >= y2) continue;
|
||||
if (x1 < 0) x1 = 0;
|
||||
if (y1 < 0) y1 = 0;
|
||||
if (x2 > self->heightmap->w) x2 = self->heightmap->w;
|
||||
if (y2 > self->heightmap->h) y2 = self->heightmap->h;
|
||||
|
||||
for (int y = y1; y < y2; y++) {
|
||||
for (int x = x1; x < x2; x++) {
|
||||
float current = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, current + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Method: multiply_bsp(bsp, ...) -> HeightMap
|
||||
PyObject* PyHeightMap::multiply_bsp(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->heightmap) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* keywords[] = {"bsp", "select", "nodes", "shrink", "value", nullptr};
|
||||
PyObject* bsp_obj = nullptr;
|
||||
const char* select_str = "leaves";
|
||||
PyObject* nodes_obj = nullptr;
|
||||
int shrink = 0;
|
||||
float value = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|sOif", const_cast<char**>(keywords),
|
||||
&bsp_obj, &select_str, &nodes_obj, &shrink, &value)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Validate bsp is a BSP
|
||||
PyObject* bsp_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "BSP");
|
||||
if (!bsp_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP type not found in module");
|
||||
return nullptr;
|
||||
}
|
||||
int is_bsp = PyObject_IsInstance(bsp_obj, bsp_type);
|
||||
Py_DECREF(bsp_type);
|
||||
|
||||
if (is_bsp < 0) return nullptr;
|
||||
if (!is_bsp) {
|
||||
PyErr_SetString(PyExc_TypeError, "multiply_bsp() requires a BSP argument");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyBSPObject* bsp = (PyBSPObject*)bsp_obj;
|
||||
if (!bsp->root) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Collect nodes
|
||||
std::vector<TCOD_bsp_t*> nodes;
|
||||
if (!collectBSPNodes(bsp, select_str, nodes_obj, nodes, "multiply_bsp")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create a mask: 0 everywhere, then set to 1 inside node regions
|
||||
// Then multiply heightmap by mask
|
||||
// Actually, for efficiency, we set cells OUTSIDE regions to 0
|
||||
|
||||
// First, create a "touched" array to track which cells are in regions
|
||||
std::vector<bool> in_region(self->heightmap->w * self->heightmap->h, false);
|
||||
|
||||
for (TCOD_bsp_t* node : nodes) {
|
||||
int x1 = node->x + shrink;
|
||||
int y1 = node->y + shrink;
|
||||
int x2 = node->x + node->w - shrink;
|
||||
int y2 = node->y + node->h - shrink;
|
||||
|
||||
// Clamp and skip if invalid
|
||||
if (x1 >= x2 || y1 >= y2) continue;
|
||||
if (x1 < 0) x1 = 0;
|
||||
if (y1 < 0) y1 = 0;
|
||||
if (x2 > self->heightmap->w) x2 = self->heightmap->w;
|
||||
if (y2 > self->heightmap->h) y2 = self->heightmap->h;
|
||||
|
||||
for (int y = y1; y < y2; y++) {
|
||||
for (int x = x1; x < x2; x++) {
|
||||
in_region[y * self->heightmap->w + x] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply: multiply by value inside regions, by 0 outside
|
||||
for (int y = 0; y < self->heightmap->h; y++) {
|
||||
for (int x = 0; x < self->heightmap->w; x++) {
|
||||
float current = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||
if (in_region[y * self->heightmap->w + x]) {
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, current * value);
|
||||
} else {
|
||||
TCOD_heightmap_set_value(self->heightmap, x, y, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,21 @@ public:
|
|||
// Subscript support for hmap[x, y] syntax
|
||||
static PyObject* subscript(PyHeightMapObject* self, PyObject* key);
|
||||
|
||||
// Combination operations (#194) - mutate self, return self for chaining
|
||||
static PyObject* add(PyHeightMapObject* self, PyObject* args);
|
||||
static PyObject* subtract(PyHeightMapObject* self, PyObject* args);
|
||||
static PyObject* multiply(PyHeightMapObject* self, PyObject* args);
|
||||
static PyObject* lerp(PyHeightMapObject* self, PyObject* args);
|
||||
static PyObject* copy_from(PyHeightMapObject* self, PyObject* args);
|
||||
static PyObject* hmap_max(PyHeightMapObject* self, PyObject* args); // 'max' conflicts with macro
|
||||
static PyObject* hmap_min(PyHeightMapObject* self, PyObject* args); // 'min' conflicts with macro
|
||||
|
||||
// Direct source sampling (#209) - sample from NoiseSource/BSP directly
|
||||
static PyObject* add_noise(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* multiply_noise(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* add_bsp(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* multiply_bsp(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Mapping methods for subscript support
|
||||
static PyMappingMethods mapping_methods;
|
||||
|
||||
|
|
|
|||
522
src/PyNoiseSource.cpp
Normal file
522
src/PyNoiseSource.cpp
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
#include "PyNoiseSource.h"
|
||||
#include "PyHeightMap.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <random>
|
||||
|
||||
// Property definitions
|
||||
PyGetSetDef PyNoiseSource::getsetters[] = {
|
||||
{"dimensions", (getter)PyNoiseSource::get_dimensions, NULL,
|
||||
MCRF_PROPERTY(dimensions, "Number of input dimensions (1-4). Read-only."), NULL},
|
||||
{"algorithm", (getter)PyNoiseSource::get_algorithm, NULL,
|
||||
MCRF_PROPERTY(algorithm, "Noise algorithm name ('simplex', 'perlin', or 'wavelet'). Read-only."), NULL},
|
||||
{"hurst", (getter)PyNoiseSource::get_hurst, NULL,
|
||||
MCRF_PROPERTY(hurst, "Hurst exponent for fbm/turbulence. Read-only."), NULL},
|
||||
{"lacunarity", (getter)PyNoiseSource::get_lacunarity, NULL,
|
||||
MCRF_PROPERTY(lacunarity, "Frequency multiplier between octaves. Read-only."), NULL},
|
||||
{"seed", (getter)PyNoiseSource::get_seed, NULL,
|
||||
MCRF_PROPERTY(seed, "Random seed used (even if originally None). Read-only."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Method definitions
|
||||
PyMethodDef PyNoiseSource::methods[] = {
|
||||
{"get", (PyCFunction)PyNoiseSource::get, METH_VARARGS,
|
||||
MCRF_METHOD(NoiseSource, get,
|
||||
MCRF_SIG("(pos: tuple[float, ...])", "float"),
|
||||
MCRF_DESC("Get flat noise value at coordinates."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Position tuple with length matching dimensions")
|
||||
MCRF_RETURNS("float: Noise value in range [-1.0, 1.0]")
|
||||
MCRF_RAISES("ValueError", "Position tuple length doesn't match dimensions")
|
||||
)},
|
||||
{"fbm", (PyCFunction)PyNoiseSource::fbm, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(NoiseSource, fbm,
|
||||
MCRF_SIG("(pos: tuple[float, ...], octaves: int = 4)", "float"),
|
||||
MCRF_DESC("Get fractal brownian motion value at coordinates."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Position tuple with length matching dimensions")
|
||||
MCRF_ARG("octaves", "Number of noise octaves to combine (default: 4)")
|
||||
MCRF_RETURNS("float: FBM noise value in range [-1.0, 1.0]")
|
||||
MCRF_RAISES("ValueError", "Position tuple length doesn't match dimensions")
|
||||
)},
|
||||
{"turbulence", (PyCFunction)PyNoiseSource::turbulence, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(NoiseSource, turbulence,
|
||||
MCRF_SIG("(pos: tuple[float, ...], octaves: int = 4)", "float"),
|
||||
MCRF_DESC("Get turbulence (absolute fbm) value at coordinates."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Position tuple with length matching dimensions")
|
||||
MCRF_ARG("octaves", "Number of noise octaves to combine (default: 4)")
|
||||
MCRF_RETURNS("float: Turbulence noise value in range [-1.0, 1.0]")
|
||||
MCRF_RAISES("ValueError", "Position tuple length doesn't match dimensions")
|
||||
)},
|
||||
{"sample", (PyCFunction)PyNoiseSource::sample, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(NoiseSource, sample,
|
||||
MCRF_SIG("(size: tuple[int, int], world_origin: tuple[float, float] = (0.0, 0.0), "
|
||||
"world_size: tuple[float, float] = None, mode: str = 'fbm', octaves: int = 4)", "HeightMap"),
|
||||
MCRF_DESC("Sample noise into a HeightMap for batch processing."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("size", "Output dimensions in cells as (width, height)")
|
||||
MCRF_ARG("world_origin", "World coordinates of top-left corner (default: (0, 0))")
|
||||
MCRF_ARG("world_size", "World area to sample (default: same as size)")
|
||||
MCRF_ARG("mode", "Sampling mode: 'flat', 'fbm', or 'turbulence' (default: 'fbm')")
|
||||
MCRF_ARG("octaves", "Octaves for fbm/turbulence modes (default: 4)")
|
||||
MCRF_RETURNS("HeightMap: New HeightMap filled with sampled noise values")
|
||||
MCRF_NOTE("Requires dimensions=2. Values are in range [-1.0, 1.0].")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Helper: Convert algorithm enum to string
|
||||
static const char* algorithm_to_string(TCOD_noise_type_t alg) {
|
||||
switch (alg) {
|
||||
case TCOD_NOISE_PERLIN: return "perlin";
|
||||
case TCOD_NOISE_SIMPLEX: return "simplex";
|
||||
case TCOD_NOISE_WAVELET: return "wavelet";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Convert string to algorithm enum
|
||||
static bool string_to_algorithm(const char* str, TCOD_noise_type_t* out) {
|
||||
if (strcmp(str, "simplex") == 0) {
|
||||
*out = TCOD_NOISE_SIMPLEX;
|
||||
return true;
|
||||
} else if (strcmp(str, "perlin") == 0) {
|
||||
*out = TCOD_NOISE_PERLIN;
|
||||
return true;
|
||||
} else if (strcmp(str, "wavelet") == 0) {
|
||||
*out = TCOD_NOISE_WAVELET;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper: Parse position tuple and validate dimensions
|
||||
static bool parse_position(PyObject* pos_obj, int expected_dims, float* coords) {
|
||||
if (!PyTuple_Check(pos_obj) && !PyList_Check(pos_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple or list");
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t size = PyTuple_Check(pos_obj) ? PyTuple_Size(pos_obj) : PyList_Size(pos_obj);
|
||||
if (size != expected_dims) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Position has %zd coordinates, but NoiseSource has %d dimensions",
|
||||
size, expected_dims);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Py_ssize_t i = 0; i < size; i++) {
|
||||
PyObject* item = PyTuple_Check(pos_obj) ? PyTuple_GetItem(pos_obj, i) : PyList_GetItem(pos_obj, i);
|
||||
if (PyFloat_Check(item)) {
|
||||
coords[i] = (float)PyFloat_AsDouble(item);
|
||||
} else if (PyLong_Check(item)) {
|
||||
coords[i] = (float)PyLong_AsLong(item);
|
||||
} else {
|
||||
PyErr_Format(PyExc_TypeError, "Coordinate %zd must be a number", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
PyObject* PyNoiseSource::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
PyNoiseSourceObject* self = (PyNoiseSourceObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->noise = nullptr;
|
||||
self->dimensions = 2;
|
||||
self->algorithm = TCOD_NOISE_SIMPLEX;
|
||||
self->hurst = TCOD_NOISE_DEFAULT_HURST;
|
||||
self->lacunarity = TCOD_NOISE_DEFAULT_LACUNARITY;
|
||||
self->seed = 0;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyNoiseSource::init(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"dimensions", "algorithm", "hurst", "lacunarity", "seed", nullptr};
|
||||
int dimensions = 2;
|
||||
const char* algorithm_str = "simplex";
|
||||
float hurst = TCOD_NOISE_DEFAULT_HURST;
|
||||
float lacunarity = TCOD_NOISE_DEFAULT_LACUNARITY;
|
||||
PyObject* seed_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|isffO", const_cast<char**>(keywords),
|
||||
&dimensions, &algorithm_str, &hurst, &lacunarity, &seed_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate dimensions
|
||||
if (dimensions < 1 || dimensions > TCOD_NOISE_MAX_DIMENSIONS) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"dimensions must be between 1 and %d, got %d",
|
||||
TCOD_NOISE_MAX_DIMENSIONS, dimensions);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse algorithm
|
||||
TCOD_noise_type_t algorithm;
|
||||
if (!string_to_algorithm(algorithm_str, &algorithm)) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"algorithm must be 'simplex', 'perlin', or 'wavelet', got '%s'",
|
||||
algorithm_str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle seed - generate random if None
|
||||
uint32_t seed;
|
||||
if (seed_obj == nullptr || seed_obj == Py_None) {
|
||||
// Generate random seed using C++ random facilities
|
||||
std::random_device rd;
|
||||
seed = rd();
|
||||
} else if (PyLong_Check(seed_obj)) {
|
||||
seed = (uint32_t)PyLong_AsUnsignedLong(seed_obj);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "seed must be an integer or None");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clean up any existing noise object
|
||||
if (self->noise) {
|
||||
TCOD_noise_delete(self->noise);
|
||||
}
|
||||
|
||||
// Create TCOD random generator with the seed
|
||||
TCOD_Random* rng = TCOD_random_new_from_seed(TCOD_RNG_MT, seed);
|
||||
if (!rng) {
|
||||
PyErr_SetString(PyExc_MemoryError, "Failed to create random generator");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create noise object
|
||||
self->noise = TCOD_noise_new(dimensions, hurst, lacunarity, rng);
|
||||
if (!self->noise) {
|
||||
TCOD_random_delete(rng);
|
||||
PyErr_SetString(PyExc_MemoryError, "Failed to create noise object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set the algorithm
|
||||
TCOD_noise_set_type(self->noise, algorithm);
|
||||
|
||||
// Store configuration
|
||||
self->dimensions = dimensions;
|
||||
self->algorithm = algorithm;
|
||||
self->hurst = hurst;
|
||||
self->lacunarity = lacunarity;
|
||||
self->seed = seed;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PyNoiseSource::dealloc(PyNoiseSourceObject* self)
|
||||
{
|
||||
if (self->noise) {
|
||||
// The TCOD_Noise owns its random generator, so deleting noise also deletes rng
|
||||
TCOD_noise_delete(self->noise);
|
||||
self->noise = nullptr;
|
||||
}
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::repr(PyObject* obj)
|
||||
{
|
||||
PyNoiseSourceObject* self = (PyNoiseSourceObject*)obj;
|
||||
std::ostringstream ss;
|
||||
|
||||
if (self->noise) {
|
||||
ss << "<NoiseSource " << self->dimensions << "D "
|
||||
<< algorithm_to_string(self->algorithm)
|
||||
<< " seed=" << self->seed << ">";
|
||||
} else {
|
||||
ss << "<NoiseSource (uninitialized)>";
|
||||
}
|
||||
|
||||
return PyUnicode_FromString(ss.str().c_str());
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
PyObject* PyNoiseSource::get_dimensions(PyNoiseSourceObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->dimensions);
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::get_algorithm(PyNoiseSourceObject* self, void* closure)
|
||||
{
|
||||
return PyUnicode_FromString(algorithm_to_string(self->algorithm));
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::get_hurst(PyNoiseSourceObject* self, void* closure)
|
||||
{
|
||||
return PyFloat_FromDouble(self->hurst);
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::get_lacunarity(PyNoiseSourceObject* self, void* closure)
|
||||
{
|
||||
return PyFloat_FromDouble(self->lacunarity);
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::get_seed(PyNoiseSourceObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromUnsignedLong(self->seed);
|
||||
}
|
||||
|
||||
// Point query methods
|
||||
|
||||
PyObject* PyNoiseSource::get(PyNoiseSourceObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->noise) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "NoiseSource not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* pos_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &pos_obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float coords[TCOD_NOISE_MAX_DIMENSIONS];
|
||||
if (!parse_position(pos_obj, self->dimensions, coords)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float value = TCOD_noise_get(self->noise, coords);
|
||||
return PyFloat_FromDouble(value);
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::fbm(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->noise) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "NoiseSource not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* keywords[] = {"pos", "octaves", nullptr};
|
||||
PyObject* pos_obj;
|
||||
int octaves = 4;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast<char**>(keywords),
|
||||
&pos_obj, &octaves)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (octaves < 1 || octaves > TCOD_NOISE_MAX_OCTAVES) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"octaves must be between 1 and %d, got %d",
|
||||
TCOD_NOISE_MAX_OCTAVES, octaves);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float coords[TCOD_NOISE_MAX_DIMENSIONS];
|
||||
if (!parse_position(pos_obj, self->dimensions, coords)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float value = TCOD_noise_get_fbm(self->noise, coords, (float)octaves);
|
||||
return PyFloat_FromDouble(value);
|
||||
}
|
||||
|
||||
PyObject* PyNoiseSource::turbulence(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->noise) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "NoiseSource not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* keywords[] = {"pos", "octaves", nullptr};
|
||||
PyObject* pos_obj;
|
||||
int octaves = 4;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast<char**>(keywords),
|
||||
&pos_obj, &octaves)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (octaves < 1 || octaves > TCOD_NOISE_MAX_OCTAVES) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"octaves must be between 1 and %d, got %d",
|
||||
TCOD_NOISE_MAX_OCTAVES, octaves);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float coords[TCOD_NOISE_MAX_DIMENSIONS];
|
||||
if (!parse_position(pos_obj, self->dimensions, coords)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float value = TCOD_noise_get_turbulence(self->noise, coords, (float)octaves);
|
||||
return PyFloat_FromDouble(value);
|
||||
}
|
||||
|
||||
// Batch sampling method - returns HeightMap
|
||||
|
||||
PyObject* PyNoiseSource::sample(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
if (!self->noise) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "NoiseSource not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// sample() only works for 2D noise
|
||||
if (self->dimensions != 2) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"sample() requires 2D NoiseSource, but this NoiseSource has %d dimensions",
|
||||
self->dimensions);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* keywords[] = {"size", "world_origin", "world_size", "mode", "octaves", nullptr};
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* origin_obj = nullptr;
|
||||
PyObject* world_size_obj = nullptr;
|
||||
const char* mode_str = "fbm";
|
||||
int octaves = 4;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOsi", const_cast<char**>(keywords),
|
||||
&size_obj, &origin_obj, &world_size_obj, &mode_str, &octaves)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse size
|
||||
int width, height;
|
||||
if (!PyTuple_Check(size_obj) || PyTuple_Size(size_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple of (width, height)");
|
||||
return nullptr;
|
||||
}
|
||||
width = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
||||
height = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
||||
if (PyErr_Occurred()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "size dimensions must be positive");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse world_origin (default: (0, 0))
|
||||
float origin_x = 0.0f, origin_y = 0.0f;
|
||||
if (origin_obj && origin_obj != Py_None) {
|
||||
if (!PyTuple_Check(origin_obj) || PyTuple_Size(origin_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "world_origin must be a tuple of (x, y)");
|
||||
return nullptr;
|
||||
}
|
||||
PyObject* ox = PyTuple_GetItem(origin_obj, 0);
|
||||
PyObject* oy = PyTuple_GetItem(origin_obj, 1);
|
||||
if (PyFloat_Check(ox)) origin_x = (float)PyFloat_AsDouble(ox);
|
||||
else if (PyLong_Check(ox)) origin_x = (float)PyLong_AsLong(ox);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_origin values must be numeric"); return nullptr; }
|
||||
if (PyFloat_Check(oy)) origin_y = (float)PyFloat_AsDouble(oy);
|
||||
else if (PyLong_Check(oy)) origin_y = (float)PyLong_AsLong(oy);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_origin values must be numeric"); return nullptr; }
|
||||
}
|
||||
|
||||
// Parse world_size (default: same as size)
|
||||
float world_w = (float)width, world_h = (float)height;
|
||||
if (world_size_obj && world_size_obj != Py_None) {
|
||||
if (!PyTuple_Check(world_size_obj) || PyTuple_Size(world_size_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "world_size must be a tuple of (width, height)");
|
||||
return nullptr;
|
||||
}
|
||||
PyObject* ww = PyTuple_GetItem(world_size_obj, 0);
|
||||
PyObject* wh = PyTuple_GetItem(world_size_obj, 1);
|
||||
if (PyFloat_Check(ww)) world_w = (float)PyFloat_AsDouble(ww);
|
||||
else if (PyLong_Check(ww)) world_w = (float)PyLong_AsLong(ww);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_size values must be numeric"); return nullptr; }
|
||||
if (PyFloat_Check(wh)) world_h = (float)PyFloat_AsDouble(wh);
|
||||
else if (PyLong_Check(wh)) world_h = (float)PyLong_AsLong(wh);
|
||||
else { PyErr_SetString(PyExc_TypeError, "world_size values must be numeric"); return nullptr; }
|
||||
}
|
||||
|
||||
// Parse mode
|
||||
enum class SampleMode { FLAT, FBM, TURBULENCE };
|
||||
SampleMode mode;
|
||||
if (strcmp(mode_str, "flat") == 0) {
|
||||
mode = SampleMode::FLAT;
|
||||
} else if (strcmp(mode_str, "fbm") == 0) {
|
||||
mode = SampleMode::FBM;
|
||||
} else if (strcmp(mode_str, "turbulence") == 0) {
|
||||
mode = SampleMode::TURBULENCE;
|
||||
} else {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"mode must be 'flat', 'fbm', or 'turbulence', got '%s'",
|
||||
mode_str);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Validate octaves
|
||||
if (octaves < 1 || octaves > TCOD_NOISE_MAX_OCTAVES) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"octaves must be between 1 and %d, got %d",
|
||||
TCOD_NOISE_MAX_OCTAVES, octaves);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create 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;
|
||||
}
|
||||
|
||||
PyObject* size_tuple = Py_BuildValue("(ii)", width, height);
|
||||
if (!size_tuple) {
|
||||
Py_DECREF(heightmap_type);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* hmap_args = PyTuple_Pack(1, size_tuple);
|
||||
Py_DECREF(size_tuple);
|
||||
if (!hmap_args) {
|
||||
Py_DECREF(heightmap_type);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyHeightMapObject* hmap = (PyHeightMapObject*)PyObject_Call(heightmap_type, hmap_args, nullptr);
|
||||
Py_DECREF(hmap_args);
|
||||
Py_DECREF(heightmap_type);
|
||||
|
||||
if (!hmap) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Sample noise into the heightmap
|
||||
// Formula: For output cell (x, y), sample world coordinate:
|
||||
// wx = world_origin[0] + (x / size[0]) * world_size[0]
|
||||
// wy = world_origin[1] + (y / size[1]) * world_size[1]
|
||||
float coords[2];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
coords[0] = origin_x + ((float)x / (float)width) * world_w;
|
||||
coords[1] = origin_y + ((float)y / (float)height) * world_h;
|
||||
|
||||
float value;
|
||||
switch (mode) {
|
||||
case SampleMode::FLAT:
|
||||
value = TCOD_noise_get(self->noise, coords);
|
||||
break;
|
||||
case SampleMode::FBM:
|
||||
value = TCOD_noise_get_fbm(self->noise, coords, (float)octaves);
|
||||
break;
|
||||
case SampleMode::TURBULENCE:
|
||||
value = TCOD_noise_get_turbulence(self->noise, coords, (float)octaves);
|
||||
break;
|
||||
}
|
||||
|
||||
TCOD_heightmap_set_value(hmap->heightmap, x, y, value);
|
||||
}
|
||||
}
|
||||
|
||||
return (PyObject*)hmap;
|
||||
}
|
||||
87
src/PyNoiseSource.h
Normal file
87
src/PyNoiseSource.h
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <libtcod.h>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declaration
|
||||
class PyNoiseSource;
|
||||
|
||||
// Python object structure for NoiseSource
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
TCOD_Noise* noise; // libtcod noise object (owned)
|
||||
int dimensions; // 1-4
|
||||
TCOD_noise_type_t algorithm; // PERLIN, SIMPLEX, or WAVELET
|
||||
float hurst; // Hurst exponent for fbm/turbulence
|
||||
float lacunarity; // Frequency multiplier between octaves
|
||||
uint32_t seed; // Random seed (stored even if auto-generated)
|
||||
} PyNoiseSourceObject;
|
||||
|
||||
class PyNoiseSource
|
||||
{
|
||||
public:
|
||||
// Python type interface
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
static int init(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds);
|
||||
static void dealloc(PyNoiseSourceObject* self);
|
||||
static PyObject* repr(PyObject* obj);
|
||||
|
||||
// Properties (all read-only)
|
||||
static PyObject* get_dimensions(PyNoiseSourceObject* self, void* closure);
|
||||
static PyObject* get_algorithm(PyNoiseSourceObject* self, void* closure);
|
||||
static PyObject* get_hurst(PyNoiseSourceObject* self, void* closure);
|
||||
static PyObject* get_lacunarity(PyNoiseSourceObject* self, void* closure);
|
||||
static PyObject* get_seed(PyNoiseSourceObject* self, void* closure);
|
||||
|
||||
// Point query methods (#207)
|
||||
static PyObject* get(PyNoiseSourceObject* self, PyObject* args);
|
||||
static PyObject* fbm(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* turbulence(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Batch sampling method (#208) - returns HeightMap
|
||||
static PyObject* sample(PyNoiseSourceObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Method and property definitions
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
inline PyTypeObject PyNoiseSourceType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.NoiseSource",
|
||||
.tp_basicsize = sizeof(PyNoiseSourceObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyNoiseSource::dealloc,
|
||||
.tp_repr = PyNoiseSource::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"NoiseSource(dimensions: int = 2, algorithm: str = 'simplex', hurst: float = 0.5, lacunarity: float = 2.0, seed: int = None)\n\n"
|
||||
"A configured noise generator for procedural generation.\n\n"
|
||||
"NoiseSource wraps libtcod's noise generator, providing coherent noise values "
|
||||
"that can be used for terrain generation, textures, and other procedural content. "
|
||||
"The same coordinates always produce the same value (deterministic).\n\n"
|
||||
"Args:\n"
|
||||
" dimensions: Number of input dimensions (1-4). Default: 2.\n"
|
||||
" algorithm: Noise algorithm - 'simplex', 'perlin', or 'wavelet'. Default: 'simplex'.\n"
|
||||
" hurst: Fractal Hurst exponent for fbm/turbulence (0.0-1.0). Default: 0.5.\n"
|
||||
" lacunarity: Frequency multiplier between octaves. Default: 2.0.\n"
|
||||
" seed: Random seed for reproducibility. None for random seed.\n\n"
|
||||
"Properties:\n"
|
||||
" dimensions (int): Read-only. Number of input dimensions.\n"
|
||||
" algorithm (str): Read-only. Noise algorithm name.\n"
|
||||
" hurst (float): Read-only. Hurst exponent.\n"
|
||||
" lacunarity (float): Read-only. Lacunarity value.\n"
|
||||
" seed (int): Read-only. Seed used (even if originally None).\n\n"
|
||||
"Example:\n"
|
||||
" noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)\n"
|
||||
" value = noise.get((10.5, 20.3)) # Returns -1.0 to 1.0\n"
|
||||
" fbm_val = noise.fbm((10.5, 20.3), octaves=6)\n"
|
||||
),
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_init = (initproc)PyNoiseSource::init,
|
||||
.tp_new = PyNoiseSource::pynew,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue