scene_service.annotations¶
User annotations — user-authored semantics anchored to the SLAM map.
An annotation is a named geometric mark the user draws on the map canvas: a room (polygon + name) or a poi (single point, optional heading). Annotations share the same foundation as perceived objects: coordinates are map-frame meters, storage is partitioned by map_id, and validity is judged by mapping’s generation epoch (see map_binding.py). Unlike perceived objects they are USER ASSETS: a map rebuild never deletes them — they are kept and flagged stale for the user to confirm or redraw.
Storage is one JSON file per map (<base_dir>/<map_id>.json, atomic tmp+rename writes). Annotations carry no vectors, so they stay out of the milvus object store on purpose; the per-map JSON mirrors SceneGraphStore’s partitioning pattern.
Functions
|
Validate user-supplied annotation fields; return an error string on the first violation, None when everything is acceptable. |
Classes
|
One user annotation. |
|
Per-map JSON persistence for user annotations. |
- class scene_service.annotations.Annotation(annotation_id: str, kind: str, name: str, points: list[list[float]] = <factory>, theta: float | None = None, created_at: float = 0.0, updated_at: float = 0.0, stale: bool = False, stale_reason: str = '')[source]¶
Bases:
objectOne user annotation. points are map-frame meters; stale means the map’s generation changed after this was drawn — geometry may no longer line up, and the user must confirm or redraw (never auto-deleted).
- annotation_id: str¶
- created_at: float = 0.0¶
- classmethod from_json(d: dict) Annotation[source]¶
Rebuild from a stored dict, dropping unknown keys so a file written by a newer scene still loads (forward-tolerant).
- kind: str¶
- name: str¶
- points: list[list[float]]¶
- stale: bool = False¶
- stale_reason: str = ''¶
- theta: float | None = None¶
- updated_at: float = 0.0¶
- class scene_service.annotations.AnnotationStore(base_dir: str, *, map_id: str, generation: int | None = None)[source]¶
Bases:
objectPer-map JSON persistence for user annotations.
One file per map: <base_dir>/<map_id>.json (map_id sanitized with the same rule as the object store so the two partitions always agree). The file records {map_id, generation, annotations}; on load, a stored generation that differs from the current binding’s means the map frame was rebuilt since the annotations were drawn — every annotation is marked stale (kept, never dropped: user assets, unlike perceived objects, are never cleaned up automatically). When either side has no generation (static binding, pre-P2 file) staleness cannot be judged and the stored value is preserved for the next boot that can.
All methods are synchronous and guarded by an internal lock; writes are atomic (tmp + os.replace) and happen on every mutation — annotation traffic is human-speed, so write-through is cheap and never loses an accepted edit.
- create(*, kind: str, name: str, points: list, theta: float | None = None) Annotation[source]¶
Create + persist an annotation and return it.
Room creation is idempotent by normalized room name: the user page can legitimately re-enter this endpoint once when double-click or Enter browser events race around the async name dialog. In that case we keep the existing annotation id and update its polygon instead of creating two visually identical rooms. POIs remain append-style assets. Fields must already be validated (validate_annotation_fields).
- delete(annotation_id: str) bool[source]¶
Remove an annotation; True when it existed. Persists on change.
- delete_map(map_id: str) bool[source]¶
Delete exactly one persisted annotation partition.
Map deletion is an explicit user action. Unlike a map-generation change, it intentionally removes the matching room/POI JSON asset; unrelated map partitions remain untouched. Clearing the current in-memory partition keeps the user page consistent when the deleted map is presently selected.
- get(annotation_id: str) Annotation | None[source]¶
- list() list[Annotation][source]¶
- list_json() list[dict][source]¶
JSON-ready dump of every annotation (the /api/state field and GET /api/annotations share this shape).
- property map_id: str¶
- mark_all_stale(reason: str, *, new_generation: int | None = None) int[source]¶
Flag every non-stale annotation stale with reason (map frame epoch changed at runtime — the _lifecycle_watch hook). Optionally records the new generation so the file reflects the epoch the staleness was judged against. Returns how many flipped; persists when anything changed (flag or generation).
- property path: Path¶
- rebind(map_id: str, *, generation: int | None = None, carry_current: bool = False) None[source]¶
Switch this store to another map partition.
carry_current=True is used by scene’s Save Map UI path: the user just named the live spatial map, so the rooms they drew in the live session must be written under the same map_id before future loads. carry_current=False is used by Load Map: discard the in-memory partition and load the target map’s annotation JSON.
- reconcile_generation(live_generation: int) int[source]¶
Called when the map’s true generation becomes known at runtime (the lifecycle watcher’s confirmation) — a store built under a static binding (generation None) cannot judge staleness at load time, so the judgement happens here instead. Adopts the epoch when the store had none; when the recorded epoch differs, flags everything stale (user assets are never auto-deleted, only flagged). Runs decision + sweep under one lock acquisition, so a concurrent create() can never be flagged by a sweep it postdates. Returns how many flipped.
- update(annotation_id: str, *, name: str | None = None, points: list | None = None, theta: float | None = None, clear_stale: bool = False) Annotation | None[source]¶
Apply the provided fields to an existing annotation (None = keep), bump updated_at, persist, and return it; None when the id is unknown. clear_stale is the user’s “confirm still valid” action — it resets both the flag and the recorded reason. Redrawing the points also clears staleness: new geometry is authored against the CURRENT map frame, so the old epoch’s doubt no longer applies.
- scene_service.annotations.validate_annotation_fields(kind: str, name: Any, points: Any, theta: Any) str | None[source]¶
Validate user-supplied annotation fields; return an error string on the first violation, None when everything is acceptable.
Rules: kind must be whitelisted; name a string ≤ MAX_NAME_LEN; points a list of finite [x, y] number pairs — exactly 1 for a poi, ≥3 for a room (a polygon needs three vertices); theta (a heading) only applies to a poi and must be None or a finite number — a region has no heading, so a room carrying theta is rejected rather than silently stored. NaN/inf anywhere is rejected so bad JSON can never poison the stored file.