Groups: objects of type "group" draw nothing; members carry the group's id in a new `group` field (groups nest). A hidden group hides everything in it (Doc::hidden_in_groups, skipped by the driver), a locked one locks it in the Studio. Removing a group moves its members up into its parent group. isokit check warns about members of missing groups. New `order` op: the whole object order at once (each id once), one undo step. Studio object list as a tree in the document order, front first: each group sits just after its members, so it draws as one block, and ground pieces higher in the list lie on top. Groups fold open and shut; every row has an eye and a lock (shown on hover or when set); a click on a group selects all of it, so a drag on the map moves it all. Group / Ungroup (Ctrl+G, Ctrl+Shift+G), Order up/down (Ctrl+] Ctrl+[, with Shift to the front / back), drag rows above, below or into a group. Group panel: name, zoom, ungroup, to front / back, lock, hide, delete with contents. MCP: group field in the object schema, order op, guidance. Tests: Rust 81 (ops: groups, order, removal; driver: nested hidden groups), Studio 131, web 100.
This commit is contained in:
1 parent
5ec164a633
commit
540b37020c
15 files changed
+573
-30
No files matched your search
@@ -606,6 +606,61 @@ with sync_playwright() as p:
|
||||
pg.wait_for_timeout(700)
|
||||
check(len(doc()["objects"]) == n5, "two undos take the routes back")
|
||||
|
||||
# groups and the drawing order in the object list: two trees grouped (Ctrl+G), the group hidden
|
||||
# by its eye, the yard (a ground area) to the front, a lamp dragged into the group, ungrouped
|
||||
order0 = [o["id"] for o in doc()["objects"]]
|
||||
row = lambda id: pg.locator(f'#objects li[data-id="{id}"]')
|
||||
row("tree-1").scroll_into_view_if_needed()
|
||||
row("tree-1").click()
|
||||
row("tree-2").click(modifiers=["Control"])
|
||||
pg.keyboard.press("Control+g")
|
||||
pg.wait_for_timeout(900)
|
||||
d = doc()
|
||||
grp = [o for o in d["objects"] if o["type"] == "group"]
|
||||
gid = grp[0]["id"] if grp else "?"
|
||||
ids1 = [o["id"] for o in d["objects"]]
|
||||
mem = sorted(o["id"] for o in d["objects"] if o.get("group") == gid)
|
||||
depth = pg.evaluate("() => getComputedStyle(document.querySelector('#objects li[data-id=\"tree-1\"]')).getPropertyValue('--d').trim()")
|
||||
check(len(grp) == 1 and mem == ["tree-1", "tree-2"] and ids1.index(gid) == ids1.index("tree-2") + 1 and depth == "1"
|
||||
and pg.locator("#right h3").inner_text().strip().endswith("Group 1"),
|
||||
f"Ctrl+G groups two trees: {gid} holds {mem}, indented ({depth}), just in front of them")
|
||||
drawn = "() => ['tree-1', 'tree-2'].map(id => document.querySelectorAll(`.iso-svg [data-obj='${id}']`).length)"
|
||||
row(gid).hover()
|
||||
row(gid).locator("[data-eye]").click()
|
||||
pg.wait_for_timeout(900)
|
||||
gone = pg.evaluate(drawn)
|
||||
row(gid).locator("[data-eye]").click()
|
||||
pg.wait_for_timeout(900)
|
||||
back = pg.evaluate(drawn)
|
||||
check(gone == [0, 0] and back[0] > 0 and back[1] > 0, f"the group's eye hides what is in it: {gone} -> {back}")
|
||||
row("yard").scroll_into_view_if_needed()
|
||||
row("yard").click()
|
||||
pg.keyboard.press("Control+Shift+BracketRight")
|
||||
pg.wait_for_timeout(900)
|
||||
check([o["id"] for o in doc()["objects"]][-1] == "yard", "Ctrl+Shift+] puts the yard (ground) in front: last in the drawing order")
|
||||
row(gid).scroll_into_view_if_needed()
|
||||
src, dst = row("lamp-1").bounding_box(), row(gid).bounding_box()
|
||||
pg.mouse.move(src["x"] + 60, src["y"] + src["height"] / 2)
|
||||
pg.mouse.down()
|
||||
pg.mouse.move(dst["x"] + 60, dst["y"] + dst["height"] / 2, steps=8)
|
||||
pg.mouse.up()
|
||||
pg.wait_for_timeout(900)
|
||||
check(obj("lamp-1").get("group") == gid, "a row dragged onto a group row goes into the group")
|
||||
row(gid).click()
|
||||
pg.keyboard.press("Control+Shift+g")
|
||||
pg.wait_for_timeout(900)
|
||||
d = doc()
|
||||
check(not any(o["type"] == "group" for o in d["objects"]) and not any(o.get("group") for o in d["objects"]) and len(d["objects"]) == n5,
|
||||
"Ctrl+Shift+G ungroups: the group goes, what was in it stays")
|
||||
pg.screenshot(path=str(OUT / "studio-groups.jpg"), type="jpeg", quality=80)
|
||||
for _ in range(6):
|
||||
pg.locator("#undo").click()
|
||||
pg.wait_for_timeout(700)
|
||||
check([o["id"] for o in doc()["objects"]] == order0 and not any(o.get("group") for o in doc()["objects"]),
|
||||
"six undos restore the order and take the group back out")
|
||||
pg.evaluate("__studio.select(null)")
|
||||
pg.wait_for_timeout(300)
|
||||
|
||||
# an agent edits over MCP: the open Studio follows
|
||||
init = mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "0"}}})
|
||||
check(init["result"]["serverInfo"]["name"] == "isokit", "MCP initialize")
|
||||
|
||||
Reference in new issue
Block a user