"""Targets the camera can attach to, and selection among them."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Hashable, List, Optional, Protocol, Sequence
from .frame import Quat, Vec3, dot, sub
[docs]
@dataclass(frozen=True)
class Pose:
position: Vec3 # stage units, world space
orientation: Quat = (1.0, 0.0, 0.0, 0.0) # (w, x, y, z)
[docs]
class TargetProvider(Protocol):
[docs]
def target_ids(self) -> Sequence[Hashable]:
"""Targets in cycling order."""
[docs]
def target_pose(self, tid: Hashable) -> Optional[Pose]:
"""World pose of a target, or None if it is not present now."""
[docs]
class CallbackTargets:
"""A TargetProvider from two callables, for apps that keep poses in their own dicts."""
def __init__(
self,
ids: Callable[[], Sequence[Hashable]],
pose: Callable[[Hashable], Optional[Pose]],
):
self._ids = ids
self._pose = pose
[docs]
def target_ids(self) -> Sequence[Hashable]:
return list(self._ids() or ())
[docs]
def target_pose(self, tid: Hashable) -> Optional[Pose]:
return self._pose(tid)
[docs]
class TargetCycler:
"""The selected target. The selection survives targets coming and going; previous and
next step through the provider's current list."""
def __init__(self, provider: TargetProvider, selected: Hashable = None):
self.provider = provider
self.selected = selected
[docs]
def ids(self) -> List[Hashable]:
return list(self.provider.target_ids())
[docs]
def pose(self) -> Optional[Pose]:
if self.selected is None:
return None
return self.provider.target_pose(self.selected)
[docs]
def step(self, direction: int) -> Hashable:
ids = self.ids()
if not ids:
return self.selected
if self.selected in ids:
i = (ids.index(self.selected) + direction) % len(ids)
else:
i = 0 if direction >= 0 else len(ids) - 1
self.selected = ids[i]
return self.selected
[docs]
def nearest(self, point: Sequence[float]) -> Hashable:
best, best_d = None, None
for tid in self.ids():
p = self.provider.target_pose(tid)
if p is None:
continue
d = sub(p.position, point)
d2 = dot(d, d)
if best_d is None or d2 < best_d:
best, best_d = tid, d2
return best
[docs]
def ensure(self, near: Sequence[float] | None = None) -> Hashable:
"""Keep the selection if it is present; otherwise pick the target nearest to near
(or the first one)."""
if self.selected is not None and self.provider.target_pose(self.selected) is not None:
return self.selected
pick = self.nearest(near) if near is not None else None
if pick is None:
ids = self.ids()
pick = ids[0] if ids else None
if pick is not None:
self.selected = pick
return self.selected