| 568 | |
| 569 | |
| 570 | def create_app(state: ViewerState, meta: dict): |
| 571 | from aiohttp import web |
| 572 | |
| 573 | static_dir = Path(__file__).parent / "static" |
| 574 | move_speed_base = meta["scene_scale"] * 0.01 |
| 575 | |
| 576 | async def index(request): |
| 577 | return web.Response( |
| 578 | text=(static_dir / "index.html").read_text(), |
| 579 | content_type="text/html", |
| 580 | ) |
| 581 | |
| 582 | async def get_meta(request): |
| 583 | return web.json_response({ |
| 584 | "num_frames": meta["num_frames"], |
| 585 | "total_points": meta["total_points"], |
| 586 | "scene_scale": meta["scene_scale"], |
| 587 | }) |
| 588 | |
| 589 | async def get_keyframes(request): |
| 590 | return web.json_response(state.animator.to_list()) |
| 591 | |
| 592 | async def ws_handler(request): |
| 593 | ws = web.WebSocketResponse() |
| 594 | await ws.prepare(request) |
| 595 | print("[ws] Client connected") |
| 596 | |
| 597 | # Send initial frame |
| 598 | try: |
| 599 | frame_data = state.render() |
| 600 | if frame_data: |
| 601 | await ws.send_bytes(frame_data) |
| 602 | print(f"[ws] Sent initial frame ({len(frame_data)} bytes)") |
| 603 | except Exception as e: |
| 604 | print(f"[ws] Error sending initial frame: {e}") |
| 605 | |
| 606 | need_render = False |
| 607 | |
| 608 | async for msg in ws: |
| 609 | if msg.type == web.WSMsgType.TEXT: |
| 610 | data = json.loads(msg.data) |
| 611 | t = data.get("type") |
| 612 | |
| 613 | if t == "orbit": |
| 614 | state.camera.orbit(data["dx"], data["dy"]) |
| 615 | need_render = True |
| 616 | elif t == "pan": |
| 617 | state.camera.pan(data["dx"], data["dy"]) |
| 618 | need_render = True |
| 619 | elif t == "zoom": |
| 620 | state.camera.zoom(data["delta"]) |
| 621 | need_render = True |
| 622 | elif t == "move": |
| 623 | speed = move_speed_base |
| 624 | if data.get("shift"): |
| 625 | speed *= 3.0 |
| 626 | d = data["dir"] |
| 627 | if d == "forward": |