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")
|
||||
|
||||
@@ -387,6 +387,22 @@ try:
|
||||
check(len(during) >= 2 and after and after[0] < 150 and tf == "",
|
||||
f"a long pinch is drawn anew {len(during)} times on the way and sharp {round(after[0]) if after else None} ms after it ends")
|
||||
pg.evaluate("__studio.view.fit()")
|
||||
# groups in the web Studio (the same ops in WASM): two bushes grouped, the group hidden, undone
|
||||
pg.locator('#objects li[data-id="bush-1"]').scroll_into_view_if_needed()
|
||||
pg.locator('#objects li[data-id="bush-1"]').click()
|
||||
pg.locator('#objects li[data-id="bush-2"]').click(modifiers=["Control"])
|
||||
pg.keyboard.press("Control+g")
|
||||
pg.wait_for_timeout(800)
|
||||
g = [o for o in doc(pg)["objects"] if o["type"] == "group"]
|
||||
members = sorted(o["id"] for o in doc(pg)["objects"] if g and o.get("group") == g[0]["id"])
|
||||
pg.evaluate("id => __studio.op({op: 'update', id: id, patch: {hidden: true}})", g[0]["id"] if g else "")
|
||||
pg.wait_for_timeout(800)
|
||||
hid = pg.evaluate("() => document.querySelectorAll(`.iso-svg [data-obj='bush-1'], .iso-svg [data-obj='bush-2']`).length")
|
||||
for _ in range(2):
|
||||
pg.locator("#undo").click()
|
||||
pg.wait_for_timeout(600)
|
||||
check(len(g) == 1 and members == ["bush-1", "bush-2"] and hid == 0 and not any(o["type"] == "group" for o in doc(pg)["objects"]),
|
||||
f"web Studio groups: {members} in {g[0]['id'] if g else None}, hidden with it, undone")
|
||||
|
||||
# ---------------------------------------------------------------- a system asking for less motion
|
||||
# (Windows: Animation effects off): the view starts still, but the button stays and turns it on
|
||||
|
||||
Reference in new issue
Block a user