Opening moves become row-honest: ← → within the row, clamped at edges
Eric asked what the ↑↓ buttons did — the honest answer was "reorder a flat list that no longer exists, and fragment your wall diagram if pushed across a row boundary" (A1, B1, A2 renders as THREE rows under the letter-grouped layout). The useful half survives as ← → "move within its row"; the endpoint clamps at row-letter boundaries so the diagram can't fragment. Tests pin both the clamp and the legitimate within-row swap; the acceptance flow's reorder step updated to match the new semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
75162c3e2e
commit
a23b97d2f9
+11
-11
@@ -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
|
||||
|
||||
@@ -48,8 +48,10 @@
|
||||
<label>D <input name="depth_in" size="5"></label>
|
||||
<span class="editactions">
|
||||
<button type="submit" class="primary">save</button>
|
||||
<button type="button" id="openingup" aria-label="move opening earlier">↑</button>
|
||||
<button type="button" id="openingdown" aria-label="move opening later">↓</button>
|
||||
<button type="button" id="openingup"
|
||||
aria-label="move opening left within its row" title="move left in its row">←</button>
|
||||
<button type="button" id="openingdown"
|
||||
aria-label="move opening right within its row" title="move right in its row">→</button>
|
||||
<button type="button" id="openingdelete" class="danger">delete opening</button>
|
||||
</span>
|
||||
</form>
|
||||
|
||||
@@ -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")
|
||||
|
||||
+42
-5
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user