Multi-select, select joined walls and fences, pond with swimming fish
Deploy web Studio / deploy (push) Successful in 1m27s
Deploy web Studio / deploy (push) Successful in 1m27s
Selection: S.sel stays the object picked last; S.selIds holds everything selected. - In the object list, Ctrl+click adds or removes an object and Shift+click selects a range. - On the map, Ctrl/Shift+click toggles an object and Shift+drag draws a box that selects what lies wholly inside it (locked and hidden objects are left out). - Ctrl+A selects everything that is not locked; Esc selects nothing. - Clicking a wall or fence a second time selects every piece of the same type joined to it; another click goes back to that one piece. - Dragging one selected object moves them all, together with the windows and doors on them, as one undo step. Arrow keys, R (turns the group about its middle), Ctrl+D, Delete, Lock and Hide all work on the whole selection. - When all selected objects share a type, the property form edits them all; fields where they differ show "mixed". - The agent's system prompt lists the selected objects. The viewer outlines several objects with selectObj([ids]) and passes the click event to onPick. A batch op now answers `ids`, one per operation, so the copies from a group duplicate get selected. Pond (Nature): natural, round or square, with a stone or grass edge, fish, lily pads, spreading ripples and reed clumps. The fish come from a new Swim primitive (script: s.swim). One matrix maps the object's frame to the screen. Each fish turns round its ellipse with a CSS rotation, so off-screen pausing and reduced motion apply as for the other animations. The house demo gets a koi pond in the back garden. Tests: ponds in every shape and rotation have the right number of fish, no NaN and no depth-sort cycles, and stay inside their rectangle. check_studio covers list and map multi-select, a group drag undone in one step, joined-fence selection, a mixed-value form applied to all, box select, delete and undo, Ctrl+A, Esc, and a new pond's fish.
This commit is contained in:
1 parent
e4017ce2a8
commit
19f4c83ae8
21 files changed
+759
-59
No files matched your search
@@ -253,6 +253,98 @@ with sync_playwright() as p:
|
||||
pg.wait_for_timeout(500)
|
||||
pg.screenshot(path=str(OUT / "studio-fences.jpg"), type="jpeg", quality=80)
|
||||
|
||||
# several objects at once: Ctrl+click in the list and on the map, a group drag as one step, Shift+drag
|
||||
# draws a box; a second click on a fence selects every fence joined to it, whose design changes for all
|
||||
sel = lambda: sorted(pg.evaluate("__studio.S.selIds"))
|
||||
at3 = lambda x, y, z: pg.evaluate(to_client, [x, y, z])
|
||||
pg.evaluate("__studio.select(null)")
|
||||
pg.locator('#objects li[data-id="truck"]').click()
|
||||
pg.locator('#objects li[data-id="genset"]').click(modifiers=["Control"])
|
||||
pg.wait_for_timeout(300)
|
||||
check(sel() == ["genset", "truck"] and "2 objects selected" in pg.locator("#right h3").inner_text(),
|
||||
f"Ctrl+click in the list adds a second object: {sel()}")
|
||||
check(pg.locator("#objects li[aria-selected=true]").count() == 2 and pg.locator(".iso-svg .iso-osel").count() >= 2,
|
||||
"both are marked in the list and outlined on the map")
|
||||
pg.evaluate("__studio.view.focus('truck', 2.5)")
|
||||
pg.wait_for_timeout(400)
|
||||
t0, g0 = obj("truck"), obj("genset")
|
||||
tb = pg.locator('.iso-svg g[data-obj="truck"] path.iso-t').first.bounding_box()
|
||||
mx, my = tb["x"] + tb["width"] / 2, tb["y"] + tb["height"] / 2
|
||||
a0, a1 = at(t0["x"], t0["y"]), at(t0["x"] + 2, t0["y"])
|
||||
pg.mouse.move(mx, my)
|
||||
pg.mouse.down()
|
||||
for i in range(1, 11):
|
||||
pg.mouse.move(mx + (a1[0] - a0[0]) * i / 10, my + (a1[1] - a0[1]) * i / 10)
|
||||
pg.wait_for_timeout(15)
|
||||
pg.mouse.up()
|
||||
pg.wait_for_timeout(800)
|
||||
check(obj("truck")["x"] == t0["x"] + 2 and obj("genset")["x"] == g0["x"] + 2 and sel() == ["genset", "truck"],
|
||||
f"dragging one moves both and keeps them selected: truck {obj('truck')['x']}, generator {obj('genset')['x']}")
|
||||
pg.locator("#undo").click()
|
||||
pg.wait_for_timeout(700)
|
||||
check(obj("truck")["x"] == t0["x"] and obj("genset")["x"] == g0["x"], "one undo puts both back")
|
||||
# the fence run: a click selects one piece, a second click every fence joined to it
|
||||
pg.evaluate("__studio.select(null)")
|
||||
pg.evaluate("__studio.view.focus([29, -9.5, 0], 3)")
|
||||
pg.wait_for_timeout(400)
|
||||
pg.mouse.click(*at3(28, -12, 0.9))
|
||||
pg.wait_for_timeout(400)
|
||||
check(sel() == [south], f"a click selects one fence: {sel()}")
|
||||
pg.mouse.click(*at3(28, -12, 0.9))
|
||||
pg.wait_for_timeout(400)
|
||||
joined = set(o["id"] for o in run) | {sp["id"], "fence-east"}
|
||||
check(set(sel()) == joined, f"a second click selects every fence joined to it ({len(sel())} pieces)")
|
||||
pg.screenshot(path=str(OUT / "studio-multi.jpg"), type="jpeg", quality=80)
|
||||
check(pg.locator('#right select[data-prop="style"]').input_value() == "" and pg.locator('#right input[type=number][data-prop="w"]').input_value() == "",
|
||||
"where the pieces differ (design, length) the form says mixed")
|
||||
pg.locator('#right select[data-prop="style"]').select_option("wood")
|
||||
pg.wait_for_timeout(800)
|
||||
check(all(obj(i)["props"].get("style") == "wood" for i in joined), "a design picked for the selection goes to all of them")
|
||||
pg.mouse.click(*at3(28, -12, 0.9))
|
||||
pg.wait_for_timeout(400)
|
||||
check(sel() == [south], "a click on one of them selects just that one again")
|
||||
# Shift+drag: a box around the run selects what lies wholly inside it
|
||||
pg.evaluate("__studio.select(null)")
|
||||
corners = [at(23, -13), at(33, -13), at(33, -6), at(23, -6)]
|
||||
x0, x1 = min(c[0] for c in corners), max(c[0] for c in corners)
|
||||
y0, y1 = min(c[1] for c in corners) - 40, max(c[1] for c in corners)
|
||||
pg.keyboard.down("Shift")
|
||||
pg.mouse.move(x0, y0)
|
||||
pg.mouse.down()
|
||||
for i in range(1, 11):
|
||||
pg.mouse.move(x0 + (x1 - x0) * i / 10, y0 + (y1 - y0) * i / 10)
|
||||
pg.wait_for_timeout(15)
|
||||
check(pg.locator("#stband").count() == 1, "Shift+drag draws a selection box")
|
||||
pg.mouse.up()
|
||||
pg.keyboard.up("Shift")
|
||||
pg.wait_for_timeout(400)
|
||||
boxed = set(sel())
|
||||
check(set(o["id"] for o in run) <= boxed and "site" not in boxed and pg.locator("#stband").count() == 0,
|
||||
f"the box selects the run's fences, not the locked ground ({len(boxed)} objects)")
|
||||
# Delete removes them all as one step
|
||||
n4 = len(doc()["objects"])
|
||||
pg.locator(".iso-svg").focus()
|
||||
pg.keyboard.press("Delete")
|
||||
pg.wait_for_timeout(800)
|
||||
check(len(doc()["objects"]) == n4 - len(boxed) and sel() == [], f"Delete removes all {len(boxed)}")
|
||||
pg.keyboard.press("Control+z")
|
||||
pg.wait_for_timeout(800)
|
||||
check(len(doc()["objects"]) == n4, "one undo brings them all back")
|
||||
pg.keyboard.press("Control+a")
|
||||
pg.wait_for_timeout(300)
|
||||
check(len(sel()) == len([o for o in doc()["objects"] if not o.get("locked") and not o.get("hidden")]), "Ctrl+A selects everything not locked")
|
||||
pg.keyboard.press("Escape")
|
||||
pg.wait_for_timeout(300)
|
||||
check(sel() == [], "Esc clears the selection")
|
||||
# a pond: fish swim round in it (CSS animation, one rotating group per fish)
|
||||
pg.evaluate("__studio.op({op: 'add', type: 'pond', x: -29, y: 13})")
|
||||
pg.wait_for_timeout(900)
|
||||
check(pg.locator('.iso-svg g[data-obj] .iso-swim').count() == 5 and pg.evaluate("getComputedStyle(document.querySelector('.iso-svg .iso-swim')).animationName") == "iso-spin",
|
||||
"a new pond has five fish swimming round")
|
||||
pg.locator("#undo").click()
|
||||
pg.wait_for_timeout(600)
|
||||
pg.evaluate("__studio.select(null)")
|
||||
|
||||
# the middle mouse button only moves the camera: it never drags, selects or places anything
|
||||
pg.locator('#objects li[data-id="truck"]').click()
|
||||
pg.evaluate("__studio.view.focus('truck', 3)")
|
||||
|
||||
Reference in new issue
Block a user