Routes: movers along waypoints (roadmap 8)
Deploy web Studio / deploy (push) Successful in 1m4s

New route prefab (routes.rhai): cars, buses, motorbikes, cyclists, people,
deer or elephants move along waypoints, round a loop or there and back.
Each way keeps to the left of the line (or right), corners are rounded
into pieces of at most 45 degrees; there-and-back routes become loops with
U-turns, so the two directions never share a strip.

Engine: Prim::Travel / sprite.begin_travel / script travel(a, b, dur,
windows) ... end(): the mover is drawn at a piece's start and slides to
its end while it is on that piece, hidden otherwise. The renderer writes
keyframes per piece (named by a hash of their content) into one <style>
per view. Each piece is its own sprite with the lane strip as its sort
extent (ext_reset), mitred at the corners, so movers pass in front of and
behind trees, lamps and houses. Movers that look alike and never meet on
a piece share one drawing. iso-travel pauses (not hides) in still,
reduced-motion and ghosted views.

Studio: meta shape "path" draws point by point (snapping like runs) and
saves one object with pts; ending on the first point closes the loop.
MCP: pts in the object schema, route guidance.

Demos: streets gets traffic round the block, a bus, motorbikes, cyclists
and strolling people (replacing the one-axis cars); country gets cars on
the lane.

Tests: Rust 79 (each mover on exactly one piece at every moment, in every
rotation), Studio 125, web 99.
This commit is contained in:
grabowski committed 2026-10-25 17:52:17 +07:00
1 parent ccfd619a99
commit 5ec164a633
20 files changed
+698 -23

No files matched your search

+29
View File
@@ -577,6 +577,35 @@ with sync_playwright() as p:
pg.wait_for_timeout(700)
check(len(doc()["objects"]) == n5, "two undos take the animals back")
# a route drawn point by point: four corners and the first again make a loop of cars; two
# points and Enter a there-and-back walk. Each is one object whose pts are the points
pg.locator('.st-tile[data-type="route"]').click()
hint = pg.locator("#hint").inner_text()
for x, y in [(42, 14), (56, 14), (56, 24), (42, 24), (42, 14)]:
pg.mouse.click(*at(x, y))
pg.wait_for_timeout(150)
pg.wait_for_timeout(1000)
loop = doc()["objects"][-1]
trav = pg.evaluate(f"() => document.querySelectorAll(`[data-obj='{loop['id']}'] .iso-travel`).length")
rows = pg.evaluate(f"() => Object.fromEntries((__studio.view.info('{loop['id']}') || {{rows: []}}).rows)")
check("point after point" in hint and loop["type"] == "route" and loop["pts"] == [[-7, -5], [7, -5], [7, 5], [-7, 5]] and loop["x"] == 49 and loop["y"] == 19
and loop["props"].get("closed") is True and trav > 4 and rows.get("Route") == "loop, 48 m",
f"a route drawn point by point loops: {loop.get('pts')} at ({loop['x']}, {loop['y']}), {trav} pieces, {rows}")
pg.locator('.st-tile[data-type="route"]').click()
for x, y in [(42, 28), (56, 28)]:
pg.mouse.click(*at(x, y))
pg.wait_for_timeout(150)
pg.keyboard.press("Enter")
pg.wait_for_timeout(1000)
walk = doc()["objects"][-1]
check(walk["type"] == "route" and walk["pts"] == [[-7, 0], [7, 0]] and walk["props"].get("closed") is False,
f"two points and Enter: there and back {walk.get('pts')}")
pg.screenshot(path=str(OUT / "studio-route.jpg"), type="jpeg", quality=80)
for _ in range(2):
pg.locator("#undo").click()
pg.wait_for_timeout(700)
check(len(doc()["objects"]) == n5, "two undos take the routes back")
# 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")
+7
View File
@@ -340,6 +340,13 @@ try:
meets = pg.evaluate("() => Object.fromEntries((__studio.view.info('cross-st') || {rows: []}).rows)['Meets'] || ''")
ring = pg.evaluate("() => Object.fromEntries((__studio.view.info('rb') || {rows: []}).rows)['Arms'] || ''")
check(meets == "east-ave, park-rd" and ring.startswith("4:"), f"streets know what they meet: cross-st meets {meets}; roundabout arms {ring}")
# route movers: each shows once at any moment (cars 4, bus 1, motorbikes 3, cyclists 2, people 3)
movers = pg.evaluate("""() => { const out = {}; for (const id of ['traffic', 'bus-line', 'motos', 'cyclists', 'strollers'])
out[id] = [...document.querySelectorAll(`.iso-svg [data-obj='${id}'] .iso-travel`)].filter(e => getComputedStyle(e).visibility === 'visible').length;
return out; }""")
traffic = pg.evaluate("() => Object.fromEntries((__studio.view.info('traffic') || {rows: []}).rows)['Movers'] || ''")
check(movers == {"traffic": 4, "bus-line": 1, "motos": 3, "cyclists": 2, "strollers": 3} and traffic.startswith("4 cars, 8 m/s"),
f"movers on their routes, each once: {movers}; {traffic}")
shot(pg, "web-streets.jpg")
# ---------------------------------------------------------------- the country demo: animals, an apiary, diagonals