1
0

feat: add authenticated realtime presence

This commit is contained in:
2026-08-11 20:22:12 -07:00
parent 16dc85a6f8
commit 92ebf8abb5
20 changed files with 3746 additions and 1 deletions
+39
View File
@@ -99,6 +99,13 @@ import {
type RobotSpec,
type RobotView,
} from "./robots.ts";
import {
createScenePeers,
type ScenePeersOptions,
} from "../realtime/scenePeers.ts";
import type { EntityPoseSnapshot } from "../realtime/types.ts";
export type OfficeRealtimePeersOptions = Omit<ScenePeersOptions, "project" | "groundAt">;
/** Shared empty, so a pack with no robots does not allocate one per call. */
const NO_ROBOTS: readonly RobotView[] = [];
@@ -222,6 +229,8 @@ export interface OfficeSceneOptions {
robots?: readonly RobotSpec[];
/** Optional local walk actor. Constructed inactive unless `walker.active` says otherwise. */
walker?: OfficeWalkerOptions;
/** Full-depth-only authoritative remote actors/vehicles in local metre coordinates. */
realtimePeers?: OfficeRealtimePeersOptions;
/** Defaults to false — the lid comes off, because that is the whole view. */
showCeilings?: boolean;
/** Fade the walls you are looking through. Defaults to true. */
@@ -306,6 +315,10 @@ export interface OfficeScene extends StageScene {
* The plan panel and the ceiling lights both consume it that way.
*/
robots(): readonly RobotView[];
upsertRemoteSnapshot(snapshot: EntityPoseSnapshot): boolean;
removeRemoteEntity(id: string): boolean;
clearRemoteEntities(): void;
remoteEntityCount(): number;
}
export function createOfficeScene(office: Office, options: OfficeSceneOptions): OfficeScene {
@@ -509,6 +522,18 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
: null;
const officeWalker = options.walker ? createOfficeWalker(plan, options.walker) : null;
if (officeWalker) scene.add(officeWalker.root);
// Live occupancy is excluded from the public-depth build rather than hidden.
// Geographic callbacks are required by the generic adapter but harmless here:
// an office subscription sends validated local poses in metre coordinates.
const realtimePeers = depth === "full" && options.realtimePeers
? createScenePeers({
...options.realtimePeers,
project: () => [0, 0],
groundAt: () => 0,
localSceneUnitsPerMetre: options.realtimePeers.localSceneUnitsPerMetre ?? 1,
})
: null;
if (realtimePeers) scene.add(realtimePeers.root);
if (robots) {
scene.add(robots.group);
/**
@@ -830,6 +855,18 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
setWalkers(walkers) {
luminaires.setWalkers(walkers);
},
upsertRemoteSnapshot: (snapshot) => {
// The generic peer adapter can also project geographic poses, but an
// office must never render a stale city stream or another office's local
// coordinates at its origin during an interest handoff.
if (snapshot.pose.space !== "local") return false;
const cell = snapshot.pose.cell;
if (!("officeId" in cell) || cell.officeId !== office.id) return false;
return realtimePeers?.upsert(snapshot) ?? false;
},
removeRemoteEntity: (id) => realtimePeers?.remove(id) ?? false,
clearRemoteEntities: () => { realtimePeers?.clear(); },
remoteEntityCount: () => realtimePeers?.count() ?? 0,
// Stepping back out to the city should retire the hover with it, or the
// detail card for whoever the pointer was over survives the journey.
onExit: () => kit.resetPick(),
@@ -840,6 +877,7 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
kit.tick(dt);
officeWalker?.tick(dt);
if (walking && officeWalker) kit.setPose(officeWalker.followPose());
realtimePeers?.tick(Date.now());
updateOcclusion();
// Robots first: the lights above them should respond to where they are
// *now*, not to where they were last frame.
@@ -849,6 +887,7 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
dispose() {
mediaSurfaces.dispose();
officeWalker?.dispose();
realtimePeers?.dispose();
robots?.dispose();
luminaires.dispose();
if (horizonPlane) {