| """ |
| Renderer protocol: the interface every lens implements over a SceneGraph. |
| |
| Each lens (character, shell3d, atmosphere, diorama) is a Renderer. The app shows |
| the available lenses at upload time, the user picks one (or "all"), and the chosen |
| renderer(s) consume the SAME SceneGraph. Renderers are independent and degradable: |
| `available()` lets a renderer declare itself unavailable (e.g. a hosted generation |
| key is missing, or WebGL is unsupported) without breaking the others. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import Protocol, runtime_checkable |
|
|
| from scene_graph import SceneGraph, SCHEMA_VERSION |
|
|
|
|
| @runtime_checkable |
| class Renderer(Protocol): |
| |
| id: str |
| |
| label: str |
| description: str |
| |
| supported_versions: tuple[str, ...] |
| |
| requires_generation: bool |
|
|
| def available(self) -> tuple[bool, str]: |
| """Return (is_available, reason). reason is shown if unavailable, e.g. |
| 'set REPLICATE_API_TOKEN to enable the painted atmosphere lens'.""" |
| ... |
|
|
| def render(self, scene: SceneGraph) -> dict: |
| """Produce the renderable output for this lens. |
| |
| Returns a dict the app knows how to display, e.g.: |
| {"kind": "iframe", "html": "..."} # 3D / character / canvas |
| {"kind": "image", "url": "..."} # generated atmosphere |
| {"kind": "composite", "layers": [...]} # the diorama |
| Renderers NEVER raise for an empty/odd scene; they degrade to a sensible |
| minimal output and note it in the returned dict under "notes". |
| """ |
| ... |
|
|
|
|
| def supports(renderer: "Renderer", scene: SceneGraph) -> bool: |
| return scene.schema_version in renderer.supported_versions |
|
|
|
|
| class LensRegistry: |
| """Holds the available lenses and resolves which to offer at upload time.""" |
|
|
| def __init__(self) -> None: |
| self._lenses: list[Renderer] = [] |
|
|
| def register(self, renderer: "Renderer") -> None: |
| self._lenses.append(renderer) |
|
|
| def all(self) -> list["Renderer"]: |
| return list(self._lenses) |
|
|
| def offerable(self, scene_version: str = SCHEMA_VERSION) -> list[dict]: |
| """The lens picker payload: every lens with its availability, so the UI can |
| show enabled/disabled states with reasons.""" |
| out = [] |
| for r in self._lenses: |
| ok, reason = r.available() |
| out.append({ |
| "id": r.id, "label": r.label, "description": r.description, |
| "requires_generation": r.requires_generation, |
| "version_ok": scene_version in r.supported_versions, |
| "available": ok and (scene_version in r.supported_versions), |
| "reason": reason if not ok else "", |
| }) |
| return out |
|
|