Compare commits

..

2 Commits

4 changed files with 37 additions and 8 deletions
+31
View File
@@ -3,6 +3,8 @@ from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
import enum import enum
import json
from typing import Any, Optional
from app.database import Base from app.database import Base
@@ -38,3 +40,32 @@ class Asset(Base):
owner = relationship("User", back_populates="assets") owner = relationship("User", back_populates="assets")
world = relationship("World", back_populates="assets") world = relationship("World", back_populates="assets")
# ------------------------------------------------------------------
# Computed properties
# ------------------------------------------------------------------
@property
def has_gltf_metadata(self) -> bool:
"""Return True when metadata_json contains glTF-specific keys.
glTF metadata is considered present when the stored JSON contains at
least one of the keys that *extract_gltf_metadata* produces
(mesh_count, vertex_count, has_animation, texture_refs,
bounding_box_min, bounding_box_max).
"""
if not self.metadata_json:
return False
try:
data: dict[str, Any] = json.loads(self.metadata_json)
except (json.JSONDecodeError, TypeError):
return False
gltf_keys = {
"mesh_count",
"vertex_count",
"has_animation",
"texture_refs",
"bounding_box_min",
"bounding_box_max",
}
return bool(gltf_keys & data.keys())
+4 -8
View File
@@ -23,9 +23,10 @@ explicitly out of scope.
| # | Type | Task | Depends on | | # | Type | Task | Depends on |
|---|------|------|-----------| |---|------|------|-----------|
| 1 | architecture | `ApiClient` autoload: HTTP + JWT storage + error contract | — | | 1 | architecture | `ApiClient` autoload: HTTP + JWT storage + error contract | — |
| 2 | implementation | Main-menu **login + register** UI built & wired (form-encoded + JSON/age gate) | 1 | | 2 | implementation | Main-menu **login** wiring (form-encoded) | 1 |
| 3 | implementation | **World list** screen consuming `GET /api/worlds` | 1 | | 3 | implementation | Main-menu **register** wiring (JSON + age gate) | 2 |
| 4 | testing | End-to-end headless smoke test + manual verify | 1,2,3 | | 4 | implementation | **World list** screen consuming `GET /api/worlds` | 2 |
| 5 | testing | End-to-end headless smoke test + manual verify | 1,3,4 |
## Design decisions captured (resolved in task 1) ## Design decisions captured (resolved in task 1)
- Token storage: in-memory singleton property for the MVP; a `save_to_disk` seam is left - Token storage: in-memory singleton property for the MVP; a `save_to_disk` seam is left
@@ -34,8 +35,3 @@ explicitly out of scope.
never crash on network failure; non-2xx surfaces `{status, detail}` from the backend. never crash on network failure; non-2xx surfaces `{status, detail}` from the backend.
- Base URL: read from a ProjectSetting, defaulting to `http://127.0.0.1:8000`. - Base URL: read from a ProjectSetting, defaulting to `http://127.0.0.1:8000`.
- Login is form-encoded; register is JSON — the client helper must support both content types. - Login is form-encoded; register is JSON — the client helper must support both content types.
## Notes
- The main-menu auth UI does **not** exist yet (main_menu.gd currently has only Play/Quit
stubs). Task 2 must **build** the login/register UI, not merely wire an existing one.
- The world-list screen also needs to be **built** (a new scene), not just wired.
+1
View File
@@ -0,0 +1 @@
uid://cjd0uhll7iylj
+1
View File
@@ -0,0 +1 @@
uid://bgnc3iamfgf6k