From 6feb2c6e638291cbf20b4960c67fd4b817d45297 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 21 Jun 2026 14:01:26 +0200 Subject: [PATCH] feat: pool set_mask + route Write a per-slot grayscale mask sidecar (img_XXXX.mask.png) and record it on the slot. Add the multipart /grid_pool/set_mask route. Co-Authored-By: Claude Opus 4.8 --- gates/pool.py | 13 +++++++++++++ gates/routes.py | 15 +++++++++++++++ tests/test_pool.py | 12 ++++++++++++ tests/test_routes_logic.py | 8 ++++++++ 4 files changed, 48 insertions(+) diff --git a/gates/pool.py b/gates/pool.py index 8fab0b5..bb42257 100644 --- a/gates/pool.py +++ b/gates/pool.py @@ -112,6 +112,19 @@ def set_label(base_dir, pool_id, index, label): return m +def set_mask(base_dir, pool_id, index, mask_bytes): + m = read_manifest(base_dir, pool_id) + if not (0 <= index < len(m["slots"])): + return m + img_name = m["slots"][index]["image"] + mask_name = img_name.replace(".png", ".mask.png") + with open(pool_dir(base_dir, pool_id) / mask_name, "wb") as f: + f.write(mask_bytes) + m["slots"][index]["mask"] = mask_name + write_manifest(base_dir, pool_id, m) + return m + + def rebuild_manifest(base_dir, pool_id): d = pool_dir(base_dir, pool_id) m = empty_manifest() diff --git a/gates/routes.py b/gates/routes.py index e22f8ab..4b530e3 100644 --- a/gates/routes.py +++ b/gates/routes.py @@ -45,6 +45,21 @@ async def _label(request): return web.json_response(handlers.handle_label(_base(), body["pool_id"], int(body["index"]), body["label"])) +@routes.post("/grid_pool/set_mask") +async def _set_mask(request): + reader = await request.multipart() + pool_id, index, data = "default", 0, None + async for part in reader: + if part.name == "pool_id": + pool_id = (await part.text()) + elif part.name == "index": + index = int(await part.text()) + elif part.name == "mask": + data = await part.read(decode=False) + m = handlers.handle_set_mask(_base(), pool_id, index, data) + return web.json_response(m) + + @routes.get("/grid_pool/list") async def _list(request): pool_id = request.query.get("pool_id", "default") diff --git a/tests/test_pool.py b/tests/test_pool.py index ddcd36b..682f9b1 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -119,3 +119,15 @@ def test_read_corrupt_manifest_triggers_rebuild(tmp_path): (d / "manifest.json").write_text("{ not json") m = pool.read_manifest(str(tmp_path), "p1") assert [s["image"] for s in m["slots"]] == ["img_0001.png"] + + +def test_set_mask_writes_sidecar(tmp_path): + pool.add_image(str(tmp_path), "p1", b"a", ts=1) + m = pool.set_mask(str(tmp_path), "p1", 0, b"MASKBYTES") + assert m["slots"][0]["mask"] == "img_0001.mask.png" + assert (tmp_path / "p1" / "img_0001.mask.png").read_bytes() == b"MASKBYTES" + + +def test_set_mask_out_of_range_noop(tmp_path): + m = pool.set_mask(str(tmp_path), "p1", 0, b"x") + assert m["slots"] == [] diff --git a/tests/test_routes_logic.py b/tests/test_routes_logic.py index 1a48e17..ea64c48 100644 --- a/tests/test_routes_logic.py +++ b/tests/test_routes_logic.py @@ -21,3 +21,11 @@ def test_handle_active_label_remove(tmp_path): assert handlers.handle_active(base, "p1", 1)["active"] == 1 assert handlers.handle_label(base, "p1", 0, "hi")["slots"][0]["label"] == "hi" assert len(handlers.handle_remove(base, "p1", 0)["slots"]) == 1 + + +def test_handle_set_mask(tmp_path): + base = str(tmp_path) + handlers.handle_add(base, "p1", _png_bytes(), "png", ts=1) + m = handlers.handle_set_mask(base, "p1", 0, b"MASKBYTES") + assert m["slots"][0]["mask"] == "img_0001.mask.png" + assert (tmp_path / "p1" / "img_0001.mask.png").read_bytes() == b"MASKBYTES"