feat(grid): numpy buffer views for ColorLayer/TileLayer via layer.edit(); closes #335

Implements the #328 bulk-edit convention for grid layers. Writable zero-copy
views of layer data are exposed ONLY through a context manager:

    with layer.edit() as view:        # ColorLayer -> (h, w, 4) uint8
        np.asarray(view)[...] = ...    # TileLayer  -> (h, w) int32 (-1 = no tile)
    # on __exit__ the whole layer is conservatively invalidated (markDirty),
    # so the edit re-renders.

Both layers already stored dense, contiguous, row-major data (std::vector<
sf::Color> / std::vector<int>; render caches chunk separately), so the views are
true zero-copy aliases. A new internal _LayerEdit type is the buffer exporter
(bf_getbuffer) and the context manager (__enter__ returns memoryview(self),
__exit__ calls GridLayer::markDirty()); it is not directly instantiable. The
conservative whole-layer invalidation on exit means no change-tracking is
needed, and it composes with the #351 render early-out (markDirty bumps
content_generation).

Independent of GridData SoA (#332): these are the render layers, not the logic
grid. A contiguous walkable/transparent view now has its storage prerequisite
from #332 but remains a separate follow-up.

Regression test issue_335_layer_edit_test.py: buffer shape/format/readonly,
zero-copy writeback both directions, the -1 tile sentinel, optional numpy
aliasing, and a screenshot check that __exit__ actually re-renders. API-surface
golden re-baselined (two new edit methods); docs/stubs regenerated; frozen
docstring gate 100%. Suite 312/312.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-07-11 02:04:42 -04:00
commit ad223b1ac0
9 changed files with 344 additions and 4 deletions

View file

@ -1,6 +1,6 @@
# McRogueFace API Reference
*Generated on 2026-07-02 20:21:29*
*Generated on 2026-07-11 02:02:32*
*This documentation was dynamically generated from the compiled module.*
@ -1276,6 +1276,15 @@ Note:
- `discovered`: Color for previously seen cells
- `unknown`: Color for never-seen cells
#### `edit() -> context manager`
Context manager yielding a zero-copy, writable view of the layer's RGBA data with shape (height, width, 4), dtype uint8. Writes to the view alias the layer's storage (no copy). On exit the whole layer is invalidated and re-rendered.
Note:
Use as `with layer.edit() as view:` -- e.g. np.asarray(view)[...] = ....
**See also:** [Bulk-edit convention](#328) (docs/api-stability.md)
#### `fill(color: Color) -> None`
Fill the entire layer with the specified color.
@ -4526,6 +4535,15 @@ Get the tile index at a cell position. Returns -1 if no tile is set.
**Raises:** IndexError: If coordinates are out of bounds
#### `edit() -> context manager`
Context manager yielding a zero-copy, writable view of the layer's tile indices with shape (height, width), dtype int32 (-1 = no tile). Writes to the view alias the layer's storage (no copy). On exit the whole layer is invalidated and re-rendered.
Note:
Use as `with layer.edit() as view:` -- e.g. np.asarray(view)[...] = ....
**See also:** [Bulk-edit convention](#328) (docs/api-stability.md)
#### `fill(index: int) -> None`
Fill the entire layer with the specified tile index.

View file

@ -108,7 +108,7 @@
<body>
<div class="container">
<h1>McRogueFace API Reference</h1>
<p><em>Generated on 2026-07-02 20:21:29</em></p>
<p><em>Generated on 2026-07-11 02:02:32</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
<p><strong>Threading:</strong> any access to mcrfpy objects from a non-main thread must happen inside <code>with mcrfpy.lock():</code>; behavior outside the lock is undefined. See <a href="threading-model.md">docs/threading-model.md</a>.</p>
@ -1417,6 +1417,15 @@ Note:</p>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">edit() -> context manager</code></h5>
<p>Context manager yielding a zero-copy, writable view of the layer&#x27;s RGBA data with shape (height, width, 4), dtype uint8. Writes to the view alias the layer&#x27;s storage (no copy). On exit the whole layer is invalidated and re-rendered.
Note:
Use as `with layer.edit() as view:` -- e.g. np.asarray(view)[...] = ....
<p class="see-also">See also: <a href="#328">Bulk-edit convention</a></p> (docs/api-stability.md)</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">fill(color: Color) -> None</code></h5>
<p>Fill the entire layer with the specified color.</p>
@ -4775,6 +4784,15 @@ Note:</p>
<p style='margin-left: 20px;'><span class='raises'>Raises:</span> IndexError: If coordinates are out of bounds</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">edit() -> context manager</code></h5>
<p>Context manager yielding a zero-copy, writable view of the layer&#x27;s tile indices with shape (height, width), dtype int32 (-1 = no tile). Writes to the view alias the layer&#x27;s storage (no copy). On exit the whole layer is invalidated and re-rendered.
Note:
Use as `with layer.edit() as view:` -- e.g. np.asarray(view)[...] = ....
<p class="see-also">See also: <a href="#328">Bulk-edit convention</a></p> (docs/api-stability.md)</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">fill(index: int) -> None</code></h5>
<p>Fill the entire layer with the specified tile index.</p>

View file

@ -14,11 +14,11 @@
. ftr VB CB
. ftr VBI CBI
.\}
.TH "MCRFPY" "3" "2026-07-02" "McRogueFace 0.2.8-6-g19d80b4" ""
.TH "MCRFPY" "3" "2026-07-11" "McRogueFace 0.2.8-32-g2ceb18e" ""
.hy
.SH McRogueFace API Reference
.PP
\f[I]Generated on 2026-07-02 20:21:29\f[R]
\f[I]Generated on 2026-07-11 02:02:32\f[R]
.PP
\f[I]This documentation was dynamically generated from the compiled
module.\f[R]
@ -1477,6 +1477,18 @@ Note:
\f[V]visible\f[R]: Color for currently visible cells -
\f[V]discovered\f[R]: Color for previously seen cells -
\f[V]unknown\f[R]: Color for never-seen cells
.SS \f[V]edit() -> context manager\f[R]
.PP
Context manager yielding a zero-copy, writable view of the layer\[cq]s
RGBA data with shape (height, width, 4), dtype uint8.
Writes to the view alias the layer\[cq]s storage (no copy).
On exit the whole layer is invalidated and re-rendered.
.PP
Note: Use as \f[V]with layer.edit() as view:\f[R] \[en]
e.g.\ np.asarray(view)[\&...]
= \&....
.PP
\f[B]See also:\f[R] Bulk-edit convention (docs/api-stability.md)
.SS \f[V]fill(color: Color) -> None\f[R]
.PP
Fill the entire layer with the specified color.
@ -4870,6 +4882,18 @@ Vector; or pass x and y separately
\f[B]Returns:\f[R] Tile index at the specified cell, or -1 if empty
.PP
\f[B]Raises:\f[R] IndexError: If coordinates are out of bounds
.SS \f[V]edit() -> context manager\f[R]
.PP
Context manager yielding a zero-copy, writable view of the layer\[cq]s
tile indices with shape (height, width), dtype int32 (-1 = no tile).
Writes to the view alias the layer\[cq]s storage (no copy).
On exit the whole layer is invalidated and re-rendered.
.PP
Note: Use as \f[V]with layer.edit() as view:\f[R] \[en]
e.g.\ np.asarray(view)[\&...]
= \&....
.PP
\f[B]See also:\f[R] Bulk-edit convention (docs/api-stability.md)
.SS \f[V]fill(index: int) -> None\f[R]
.PP
Fill the entire layer with the specified tile index.

View file

@ -683,7 +683,129 @@ void TileLayer::render(sf::RenderTarget& target,
// Python API - ColorLayer
// =============================================================================
// ============================================================================
// #335 - layer.edit() zero-copy buffer context manager
// with layer.edit() as view: # ColorLayer -> (h,w,4) uint8; TileLayer -> (h,w) int32
// view[...] = ... # writes alias the layer's data (no copy)
// # on __exit__ the whole layer is conservatively invalidated (markDirty),
// # so the edit re-renders. Writable views are ONLY reachable via edit() (#328).
// ============================================================================
int PyGridLayerAPI::LayerEdit_getbuffer(PyObject* exporter, Py_buffer* view, int flags) {
PyLayerEditObject* self = (PyLayerEditObject*)exporter;
GridLayer* L = self->layer.get();
if (!L) {
PyErr_SetString(PyExc_RuntimeError, "layer edit view is no longer valid");
view->obj = nullptr;
return -1;
}
const Py_ssize_t w = L->grid_x;
const Py_ssize_t h = L->grid_y;
if (L->type == GridLayerType::Color) {
ColorLayer* cl = static_cast<ColorLayer*>(L);
view->buf = cl->colors.data(); // sf::Color == 4 contiguous uint8 (r,g,b,a)
view->len = w * h * 4;
view->itemsize = 1;
view->format = (flags & PyBUF_FORMAT) ? const_cast<char*>("B") : nullptr;
view->ndim = 3;
self->shape[0] = h; self->shape[1] = w; self->shape[2] = 4;
self->strides[0] = w * 4; self->strides[1] = 4; self->strides[2] = 1;
} else {
TileLayer* tl = static_cast<TileLayer*>(L);
view->buf = tl->tiles.data();
view->len = w * h * (Py_ssize_t)sizeof(int);
view->itemsize = sizeof(int);
view->format = (flags & PyBUF_FORMAT) ? const_cast<char*>("i") : nullptr; // int32
view->ndim = 2;
self->shape[0] = h; self->shape[1] = w;
self->strides[0] = w * (Py_ssize_t)sizeof(int); self->strides[1] = sizeof(int);
}
view->obj = exporter;
Py_INCREF(exporter);
view->readonly = 0;
view->shape = self->shape;
view->strides = (flags & PyBUF_STRIDES) ? self->strides : nullptr;
view->suboffsets = nullptr;
view->internal = nullptr;
return 0;
}
PyObject* PyGridLayerAPI::LayerEdit_enter(PyObject* self, PyObject* Py_UNUSED(args)) {
PyLayerEditObject* e = (PyLayerEditObject*)self;
if (!e->layer) {
PyErr_SetString(PyExc_RuntimeError, "layer edit view is no longer valid");
return nullptr;
}
e->active = true;
return PyMemoryView_FromObject(self); // memoryview holds a ref to self (the exporter)
}
PyObject* PyGridLayerAPI::LayerEdit_exit(PyObject* self, PyObject* args) {
(void)args;
PyLayerEditObject* e = (PyLayerEditObject*)self;
if (e->active && e->layer) {
e->layer->markDirty(); // #328 conservative whole-layer invalidation (bumps content_generation, #351)
}
e->active = false;
Py_RETURN_FALSE; // do not suppress exceptions raised inside the with-block
}
void PyGridLayerAPI::LayerEdit_dealloc(PyObject* self) {
PyLayerEditObject* e = (PyLayerEditObject*)self;
Py_XDECREF(e->layer_pyobj);
e->layer.reset();
Py_TYPE(self)->tp_free(self);
}
PyMethodDef PyGridLayerAPI::LayerEdit_methods[] = {
{"__enter__", (PyCFunction)PyGridLayerAPI::LayerEdit_enter, METH_NOARGS,
MCRF_METHOD(_LayerEdit, __enter__,
MCRF_SIG("()", "memoryview"),
MCRF_DESC("Enter the edit context; returns a writable zero-copy view of the layer data.")
)},
{"__exit__", (PyCFunction)PyGridLayerAPI::LayerEdit_exit, METH_VARARGS,
MCRF_METHOD(_LayerEdit, __exit__,
MCRF_SIG("(exc_type, exc_value, traceback)", "bool"),
MCRF_DESC("Exit the edit context; conservatively invalidates the whole layer so the edit re-renders.")
)},
{NULL}
};
PyBufferProcs PyGridLayerAPI::LayerEdit_as_buffer = {
.bf_getbuffer = PyGridLayerAPI::LayerEdit_getbuffer,
.bf_releasebuffer = nullptr,
};
static PyObject* makeLayerEdit(std::shared_ptr<GridLayer> layer, PyObject* layer_pyobj) {
auto* e = (PyLayerEditObject*)mcrfpydef::PyLayerEditType.tp_alloc(&mcrfpydef::PyLayerEditType, 0);
if (!e) return nullptr;
e->layer = layer; // tp_alloc zero-inits -> valid empty shared_ptr to assign into
e->layer_pyobj = layer_pyobj;
Py_XINCREF(layer_pyobj);
e->active = false;
return (PyObject*)e;
}
PyObject* PyGridLayerAPI::ColorLayer_edit(PyColorLayerObject* self, PyObject* Py_UNUSED(args)) {
if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "ColorLayer not initialized"); return nullptr; }
return makeLayerEdit(self->data, (PyObject*)self);
}
PyObject* PyGridLayerAPI::TileLayer_edit(PyTileLayerObject* self, PyObject* Py_UNUSED(args)) {
if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "TileLayer not initialized"); return nullptr; }
return makeLayerEdit(self->data, (PyObject*)self);
}
PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
{"edit", (PyCFunction)PyGridLayerAPI::ColorLayer_edit, METH_NOARGS,
MCRF_METHOD(ColorLayer, edit,
MCRF_SIG("()", "context manager"),
MCRF_DESC("Context manager yielding a zero-copy, writable view of the layer's "
"RGBA data with shape (height, width, 4), dtype uint8. Writes to the "
"view alias the layer's storage (no copy). On exit the whole layer is "
"invalidated and re-rendered.")
MCRF_NOTE("Use as `with layer.edit() as view:` -- e.g. np.asarray(view)[...] = ....")
MCRF_LINK("docs/api-stability.md", "Bulk-edit convention (#328)")
)},
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(ColorLayer, at,
MCRF_SIG("(pos: tuple | Vector) or (x: int, y: int)", "Color"),
@ -1779,6 +1901,16 @@ PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
// =============================================================================
PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
{"edit", (PyCFunction)PyGridLayerAPI::TileLayer_edit, METH_NOARGS,
MCRF_METHOD(TileLayer, edit,
MCRF_SIG("()", "context manager"),
MCRF_DESC("Context manager yielding a zero-copy, writable view of the layer's "
"tile indices with shape (height, width), dtype int32 (-1 = no tile). "
"Writes to the view alias the layer's storage (no copy). On exit the "
"whole layer is invalidated and re-rendered.")
MCRF_NOTE("Use as `with layer.edit() as view:` -- e.g. np.asarray(view)[...] = ....")
MCRF_LINK("docs/api-stability.md", "Bulk-edit convention (#328)")
)},
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(TileLayer, at,
MCRF_SIG("(pos: tuple | Vector) or (x: int, y: int)", "int"),

View file

@ -193,6 +193,19 @@ typedef struct {
std::shared_ptr<UIGrid> grid;
} PyTileLayerObject;
// #335 - context manager returned by ColorLayer.edit()/TileLayer.edit().
// Supports the buffer protocol (zero-copy numpy/memoryview view of the layer's
// dense data) and __enter__/__exit__; __exit__ conservatively invalidates the
// whole layer via markDirty() (#328). Not directly instantiable from Python.
typedef struct {
PyObject_HEAD
std::shared_ptr<GridLayer> layer;
PyObject* layer_pyobj; // strong ref keeping the layer's Python wrapper alive
Py_ssize_t shape[3];
Py_ssize_t strides[3];
bool active;
} PyLayerEditObject;
// Python API classes
class PyGridLayerAPI {
public:
@ -249,6 +262,17 @@ public:
static int TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value);
static PyMappingMethods TileLayer_mapping_methods;
// #335 - layer.edit() context manager: zero-copy writable numpy/memoryview
// view of layer data; __exit__ conservatively invalidates (markDirty()).
static PyObject* ColorLayer_edit(PyColorLayerObject* self, PyObject* Py_UNUSED(args));
static PyObject* TileLayer_edit(PyTileLayerObject* self, PyObject* Py_UNUSED(args));
static PyObject* LayerEdit_enter(PyObject* self, PyObject* Py_UNUSED(args));
static PyObject* LayerEdit_exit(PyObject* self, PyObject* args);
static int LayerEdit_getbuffer(PyObject* exporter, Py_buffer* view, int flags);
static void LayerEdit_dealloc(PyObject* self);
static PyMethodDef LayerEdit_methods[];
static PyBufferProcs LayerEdit_as_buffer;
// Method and getset arrays
static PyMethodDef ColorLayer_methods[];
static PyGetSetDef ColorLayer_getsetters[];
@ -369,4 +393,24 @@ namespace mcrfpydef {
return (PyObject*)self;
}
};
// #335 - layer.edit() context manager / buffer exporter
inline PyTypeObject PyLayerEditType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy._LayerEdit",
.tp_basicsize = sizeof(PyLayerEditObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyGridLayerAPI::LayerEdit_dealloc,
.tp_as_buffer = &PyGridLayerAPI::LayerEdit_as_buffer,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"Context manager returned by ColorLayer.edit() / TileLayer.edit().\n\n"
"Use `with layer.edit() as view:` to obtain a zero-copy, writable\n"
"numpy/memoryview of the layer's data (ColorLayer -> (h, w, 4) uint8;\n"
"TileLayer -> (h, w) int32). On exit the whole layer is invalidated so\n"
"the change is re-rendered. Not directly instantiable."
),
.tp_methods = PyGridLayerAPI::LayerEdit_methods,
.tp_new = NULL, // internal only
};
}

View file

@ -557,6 +557,9 @@ PyObject* PyInit_mcrfpy()
/*game map data - returned by Grid.at() but not directly instantiable*/
&PyUIGridPointType,
/*#335: layer.edit() context manager - returned by ColorLayer/TileLayer.edit(), not instantiable*/
&mcrfpydef::PyLayerEditType,
/*3D navigation grid - returned by Viewport3D.at() but not directly instantiable*/
&mcrfpydef::PyVoxelPointType,

View file

@ -522,6 +522,9 @@ class ColorLayer:
def draw_fov(self, source: tuple, radius: int | None = None, fov: FOV | None = None, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None:
"""Paint cells based on field-of-view visibility from a source position."""
...
def edit(self) -> context manager:
"""Context manager yielding a zero-copy, writable view of the layer's RGBA data with shape (height, width, 4), dtype uint8. Writes to the view alias the layer's..."""
...
def fill(self, color: Color) -> None:
"""Fill the entire layer with the specified color."""
...
@ -1545,6 +1548,9 @@ class TileLayer:
def at(self, *args, **kwargs) -> Any:
"""at(pos: tuple | Vector) or (x: int, y: int) -> int"""
...
def edit(self) -> context manager:
"""Context manager yielding a zero-copy, writable view of the layer's tile indices with shape (height, width), dtype int32 (-1 = no tile). Writes to the view al..."""
...
def fill(self, index: int) -> None:
"""Fill the entire layer with the specified tile index."""
...

View file

@ -0,0 +1,93 @@
"""Regression test for #335 - ColorLayer/TileLayer numpy buffer views via edit().
Per the #328 bulk-edit convention, writable zero-copy views of layer data are
exposed only through `with layer.edit() as view:`. ColorLayer yields an
(h, w, 4) uint8 view; TileLayer yields an (h, w) int32 view (-1 = no tile).
Writes alias the layer storage (no copy); on __exit__ the whole layer is
conservatively invalidated so the edit re-renders.
Verifies buffer shape/format, zero-copy writeback both directions, the -1
sentinel, optional numpy aliasing, and that exiting the context actually
re-renders (screenshot diff -- the edit->markDirty->#351 early-out loop).
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
import tempfile
TMPDIR = tempfile.mkdtemp(prefix="mcrf335_")
_n = 0
def shot():
global _n
p = os.path.join(TMPDIR, "s%d.png" % _n)
_n += 1
automation.screenshot(p)
with open(p, "rb") as f:
return f.read()
def main():
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
scene = mcrfpy.Scene("t335")
scene.activate()
cl = mcrfpy.ColorLayer(name="fog", z_index=1)
tl = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=tex)
grid = mcrfpy.Grid(grid_size=(8, 6), pos=(10, 10), size=(256, 192),
texture=tex, layers=[tl, cl])
scene.children.append(grid)
# --- ColorLayer: (h, w, 4) uint8, writable, zero-copy both directions ---
with cl.edit() as view:
assert view.shape == (6, 8, 4), f"color shape {view.shape}"
assert view.format == 'B', f"color format {view.format}"
assert view.readonly is False
view[1, 2, 0] = 200 # y=1, x=2, red channel
assert cl.at(2, 1).r == 200, "color view -> layer writeback failed"
# layer -> view direction
cl.set((3, 4), mcrfpy.Color(7, 8, 9, 255)) # x=3, y=4
with cl.edit() as view:
assert view[4, 3, 0] == 7 and view[4, 3, 2] == 9, "layer -> view read failed"
# --- TileLayer: (h, w) int32, -1 sentinel ---
with tl.edit() as view:
assert view.shape == (6, 8), f"tile shape {view.shape}"
assert view.format == 'i', f"tile format {view.format}"
view[5, 6] = 42 # y=5, x=6
view[0, 0] = -1
assert tl.at(6, 5) == 42, "tile view -> layer writeback failed"
assert tl.at(0, 0) == -1, "tile -1 sentinel round-trip failed"
# --- Optional numpy zero-copy aliasing ---
try:
import numpy as np
with tl.edit() as view:
a = np.asarray(view)
assert a.shape == (6, 8) and a.dtype == np.int32, (a.shape, a.dtype)
a[3, 3] = 17
assert tl.at(3, 3) == 17, "numpy write did not alias TileLayer"
print(" numpy zero-copy verified")
except ImportError:
print(" numpy not bundled; memoryview path verified")
# --- __exit__ invalidation actually re-renders (edit -> markDirty -> #351) ---
before = shot()
with cl.edit() as view:
# paint a visible opaque block through the view
for y in range(6):
for x in range(8):
view[y, x, 0] = 255
view[y, x, 3] = 255
after = shot()
assert before != after, "layer edit did not re-render on __exit__"
print("PASS")
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -394,6 +394,7 @@ submodule automation
meth at :: at(pos: tuple | Vector) or (x: int, y: int) -> Color
meth clear_perspective :: clear_perspective() -> None
meth draw_fov :: draw_fov(source: tuple, radius: int | None = None, fov: FOV | None = None, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None
meth edit :: edit() -> context manager
meth fill :: fill(color: Color) -> None
meth fill_rect :: fill_rect(pos: tuple, size: tuple, color: Color) -> None
meth set :: set(pos, color: Color) -> None
@ -824,6 +825,7 @@ submodule automation
meth apply_ranges :: apply_ranges(source: HeightMap, ranges: list) -> TileLayer
meth apply_threshold :: apply_threshold(source: HeightMap, range: tuple, tile: int) -> TileLayer
meth at :: at(pos: tuple | Vector) or (x: int, y: int) -> int
meth edit :: edit() -> context manager
meth fill :: fill(index: int) -> None
meth fill_rect :: fill_rect(pos: tuple, size: tuple, index: int) -> None
meth set :: set(pos, index: int) -> None