diff --git a/data/furniture.json b/data/furniture.json index 19c2710..b509381 100644 --- a/data/furniture.json +++ b/data/furniture.json @@ -22,7 +22,7 @@ { "id": "library-kallax-b1", "label": "B1", - "zone": "cubes", + "zone": "party", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -30,7 +30,7 @@ { "id": "library-kallax-b2", "label": "B2", - "zone": "cubes", + "zone": "party", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -38,7 +38,7 @@ { "id": "library-kallax-b3", "label": "B3", - "zone": "cubes", + "zone": "party", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -46,7 +46,7 @@ { "id": "library-kallax-b4", "label": "B4", - "zone": "cubes", + "zone": "two-player", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -54,7 +54,7 @@ { "id": "library-kallax-c1", "label": "C1", - "zone": "cubes", + "zone": "general", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -62,7 +62,7 @@ { "id": "library-kallax-c2", "label": "C2", - "zone": "cubes", + "zone": "general", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -70,7 +70,7 @@ { "id": "library-kallax-c3", "label": "C3", - "zone": "cubes", + "zone": "general", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -78,7 +78,7 @@ { "id": "library-kallax-c4", "label": "C4", - "zone": "cubes", + "zone": "general", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -86,7 +86,7 @@ { "id": "library-kallax-d1", "label": "D1", - "zone": "cubes", + "zone": "general", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -94,7 +94,7 @@ { "id": "library-kallax-d2", "label": "D2", - "zone": "cubes", + "zone": "vintage wargames", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 @@ -102,7 +102,7 @@ { "id": "library-kallax-d3", "label": "D3", - "zone": "cubes", + "zone": "heavy", "width_in": 13.25, "height_in": 13.25, "depth_in": 15.4 diff --git a/src/bggpipe/templates/pages/shelves.html b/src/bggpipe/templates/pages/shelves.html index 8ecd073..869ecd6 100644 --- a/src/bggpipe/templates/pages/shelves.html +++ b/src/bggpipe/templates/pages/shelves.html @@ -48,8 +48,10 @@ - - + + diff --git a/src/bggpipe/webreview.py b/src/bggpipe/webreview.py index 241c035..694dc46 100644 --- a/src/bggpipe/webreview.py +++ b/src/bggpipe/webreview.py @@ -20,6 +20,7 @@ import hashlib import io import json import os +import re import secrets import socket import threading @@ -1329,14 +1330,25 @@ def create_app( @app.post("/api/furniture/move-opening") def api_move_opening(body: OpeningMoveBody) -> dict: + """Move an opening within its ROW. The rendered layout groups + rows by label letter, so a move across a letter boundary would + fragment the wall diagram (A1, B1, A2 renders as three rows) — + the move clamps there instead.""" + + def row_of(label: str) -> str: + m = re.match(r"^([A-Za-z]+)\d+$", label or "") + return m.group(1).upper() if m else f"~{label}" + def move(units): for unit in units: ids = [o["id"] for o in unit["openings"]] if body.id in ids: + ops = unit["openings"] i = ids.index(body.id) j = i + (1 if body.direction > 0 else -1) - if 0 <= j < len(unit["openings"]): - ops = unit["openings"] + if 0 <= j < len(ops) and row_of(ops[i]["label"]) == row_of( + ops[j]["label"] + ): ops[i], ops[j] = ops[j], ops[i] return raise HTTPException(404, "no such opening") diff --git a/tests/test_shelves.py b/tests/test_shelves.py index 69b1ce8..44038d7 100644 --- a/tests/test_shelves.py +++ b/tests/test_shelves.py @@ -268,16 +268,19 @@ def test_acceptance_full_furniture_flow_without_touching_json(tmp_path): assert saved[0]["openings"][0]["label"] == "wide left" assert saved[-1]["openings"][0].get("width_in") is None # virtual - # editable, deletable, reorderable + # editable, deletable, reorderable (within a row — moves clamp at + # row-letter boundaries so the wall diagram can't fragment) first_cube = saved[0]["openings"][2]["id"] + second_cube = saved[0]["openings"][3]["id"] web.post("/api/furniture/edit-opening", json={"id": first_cube, "zone": "kids"}) web.post("/api/furniture/move-opening", json={"id": first_cube, "direction": -1}) - web.post( - "/api/furniture/delete-opening", json={"id": saved[0]["openings"][3]["id"]} - ) + saved_mid = json.loads(cfg.furniture_path.read_text())["units"] + assert saved_mid[0]["openings"][2]["id"] == first_cube # clamped: row edge + web.post("/api/furniture/move-opening", json={"id": second_cube, "direction": -1}) + web.post("/api/furniture/delete-opening", json={"id": first_cube}) saved2 = json.loads(cfg.furniture_path.read_text())["units"] assert len(saved2[0]["openings"]) == 13 - assert saved2[0]["openings"][1]["id"] == first_cube # moved up one + assert saved2[0]["openings"][2]["id"] == second_cube # swapped within row def test_locate_flow_and_stored_in_refusal(tmp_path): @@ -599,6 +602,40 @@ def test_second_grid_continues_row_letters(tmp_path): assert len(set(labels)) == 6 # unique: label-addressed CSVs stay usable +def test_move_clamps_at_row_boundaries(tmp_path): + """A move must never fragment a row: B1 cannot cross above A2 (the + renderer would draw A, B, A as three rows), but B2 and B1 can swap.""" + web, cfg = _web(tmp_path, {}) + web.post("/api/furniture/add-unit", json={"name": "Wall"}) + web.post( + "/api/furniture/add-openings", + json={ + "unit": "Wall", + "rows": 2, + "cols": 2, + "width_in": 13.0, + "height_in": 13.0, + "depth_in": 15.0, + }, + ) + + def labels(): + return [ + o["label"] for o in web.get("/api/shelves").json()["units"][0]["openings"] + ] + + ids = { + o["label"]: o["id"] + for o in web.get("/api/shelves").json()["units"][0]["openings"] + } + # cross-row move clamps: B1 stays put + web.post("/api/furniture/move-opening", json={"id": ids["B1"], "direction": -1}) + assert labels() == ["A1", "A2", "B1", "B2"] + # within-row move works: B2 left of B1 + web.post("/api/furniture/move-opening", json={"id": ids["B2"], "direction": -1}) + assert labels() == ["A1", "A2", "B2", "B1"] + + def test_move_opening_boundaries_no_op(tmp_path): web, cfg = _web(tmp_path, {}) web.post("/api/furniture/add-unit", json={"name": "Den"})