@agent-os-lab/agent-game-sdk 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,6 +62,9 @@ export async function mountAgentGameOffice(
62
62
  if (options.focusedAgentId !== undefined) {
63
63
  controller.focusAgent?.(options.focusedAgentId);
64
64
  }
65
+ if (options.focusedFloorId !== undefined) {
66
+ controller.focusFloor?.(options.focusedFloorId);
67
+ }
65
68
 
66
69
  return {
67
70
  updateAgents(agents: AgentPresence[]) {
@@ -69,12 +72,24 @@ export async function mountAgentGameOffice(
69
72
  mutableSource.updateAgents(agents);
70
73
  }
71
74
  },
75
+ resetCamera() {
76
+ controller?.resetCamera?.();
77
+ },
72
78
  focusAgent(agentId) {
73
79
  controller?.focusAgent?.(agentId);
74
80
  },
81
+ focusFloor(floorId) {
82
+ controller?.focusFloor?.(floorId);
83
+ },
84
+ getFocusedFloor() {
85
+ return controller?.getFocusedFloor?.() ?? null;
86
+ },
75
87
  refreshAgents() {
76
88
  runtimeSubscription?.refresh();
77
89
  },
90
+ getNavigationErrors() {
91
+ return controller?.getNavigationErrors?.() ?? [];
92
+ },
78
93
  destroy() {
79
94
  unsubscribe();
80
95
  runtimeSubscription?.close();
@@ -59,6 +59,10 @@ export function AgentGameOfficeView({
59
59
  viewRef.current?.focusAgent(options.focusedAgentId ?? null);
60
60
  }, [options.focusedAgentId]);
61
61
 
62
+ useEffect(() => {
63
+ viewRef.current?.focusFloor(options.focusedFloorId ?? null);
64
+ }, [options.focusedFloorId]);
65
+
62
66
  return React.createElement("div", {
63
67
  ref: containerRef,
64
68
  className,
@@ -75,8 +79,20 @@ function createOfficeConfigKey(office: AgentGameOfficeMountOptions["office"]): s
75
79
  if (!office) {
76
80
  return "default";
77
81
  }
78
- return JSON.stringify(office.rooms.map((room) => ({
79
- type: room.type,
80
- capacity: room.capacity ?? null,
81
- })));
82
+ return JSON.stringify({
83
+ floors: office.building.floors.map((floor) => ({
84
+ id: floor.id,
85
+ name: floor.name,
86
+ rooms: floor.rooms.map((room) => ({
87
+ type: room.type,
88
+ capacity: room.capacity ?? null,
89
+ })),
90
+ })),
91
+ connectors: office.building.connectors.map((connector) => ({
92
+ id: connector.id,
93
+ name: connector.name,
94
+ type: connector.type,
95
+ serves: connector.serves,
96
+ })),
97
+ });
82
98
  }
@@ -9,6 +9,7 @@ export type AgentBodyInstanceRecord = {
9
9
  mesh: AgentMeshParts;
10
10
  renderIndex: number;
11
11
  visible: boolean;
12
+ opacity?: number;
12
13
  };
13
14
 
14
15
  type AgentBodyBatch = {
@@ -17,8 +18,8 @@ type AgentBodyBatch = {
17
18
  count: number;
18
19
  };
19
20
 
20
- const geometryCache = new Map<string, THREE.BoxGeometry>();
21
- const materialCache = new Map<number, THREE.MeshLambertMaterial>();
21
+ const geometryCache = new Map<string, THREE.BufferGeometry>();
22
+ const materialCache = new Map<string, THREE.MeshLambertMaterial>();
22
23
  const matrixHelper = new THREE.Matrix4();
23
24
 
24
25
  export class AgentBodyInstancedLayer {
@@ -41,7 +42,7 @@ export class AgentBodyInstancedLayer {
41
42
  }
42
43
  record.mesh.group.updateMatrixWorld(true);
43
44
  resolveAgentBodyPartRenderSpecs(record.renderIndex).forEach((part) => {
44
- const batch = this.ensureBatch(part, records.length * 2);
45
+ const batch = this.ensureBatch(part, records.length * 2, record.opacity ?? 1);
45
46
  const object = record.mesh[part.key];
46
47
  object.updateMatrixWorld(true);
47
48
  matrixHelper.copy(object.matrixWorld);
@@ -67,8 +68,10 @@ export class AgentBodyInstancedLayer {
67
68
  private ensureBatch(
68
69
  part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number],
69
70
  requiredCapacity: number,
71
+ opacity: number,
70
72
  ): AgentBodyBatch {
71
- const key = `${part.width}:${part.height}:${part.depth}:${part.color}`;
73
+ const shape = part.shape ?? "box";
74
+ const key = `${shape}:${part.width}:${part.height}:${part.depth}:${part.color}:${opacity}`;
72
75
  const existing = this.batches.get(key);
73
76
  if (existing && existing.capacity >= requiredCapacity) {
74
77
  return existing;
@@ -78,8 +81,8 @@ export class AgentBodyInstancedLayer {
78
81
  }
79
82
 
80
83
  const mesh = new THREE.InstancedMesh(
81
- getGeometry(part.width, part.height, part.depth),
82
- getMaterial(part.color),
84
+ getGeometry(part),
85
+ getMaterial(part.color, opacity),
83
86
  Math.max(requiredCapacity, 1),
84
87
  );
85
88
  mesh.castShadow = false;
@@ -98,23 +101,45 @@ export function createAgentBodyInstancedLayer(scene: THREE.Scene): AgentBodyInst
98
101
  return new AgentBodyInstancedLayer(scene);
99
102
  }
100
103
 
101
- function getGeometry(width: number, height: number, depth: number): THREE.BoxGeometry {
102
- const key = `${width}:${height}:${depth}`;
104
+ function getGeometry(part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number]): THREE.BufferGeometry {
105
+ const shape = part.shape ?? "box";
106
+ const key = `${shape}:${part.width}:${part.height}:${part.depth}`;
103
107
  const cached = geometryCache.get(key);
104
108
  if (cached) {
105
109
  return cached;
106
110
  }
107
- const geometry = new THREE.BoxGeometry(width, height, depth);
111
+ const geometry = createGeometry(part);
108
112
  geometryCache.set(key, geometry);
109
113
  return geometry;
110
114
  }
111
115
 
112
- function getMaterial(color: number): THREE.MeshLambertMaterial {
113
- const cached = materialCache.get(color);
116
+ function createGeometry(part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number]): THREE.BufferGeometry {
117
+ switch (part.shape) {
118
+ case "frontPlane":
119
+ return new THREE.PlaneGeometry(part.width, part.height);
120
+ case "backPlane": {
121
+ const geometry = new THREE.PlaneGeometry(part.width, part.height);
122
+ geometry.rotateY(Math.PI);
123
+ return geometry;
124
+ }
125
+ case "box":
126
+ case undefined:
127
+ return new THREE.BoxGeometry(part.width, part.height, part.depth);
128
+ }
129
+ }
130
+
131
+ function getMaterial(color: number, opacity: number): THREE.MeshLambertMaterial {
132
+ const key = `${color}:${opacity}`;
133
+ const cached = materialCache.get(key);
114
134
  if (cached) {
115
135
  return cached;
116
136
  }
117
- const material = new THREE.MeshLambertMaterial({ color });
118
- materialCache.set(color, material);
137
+ const material = new THREE.MeshLambertMaterial({
138
+ color,
139
+ transparent: opacity < 1,
140
+ opacity,
141
+ depthWrite: opacity >= 1,
142
+ });
143
+ materialCache.set(key, material);
119
144
  return material;
120
145
  }
@@ -5,6 +5,7 @@ import type { AgentMeshParts } from "./agent-mesh";
5
5
  export type AgentEffectInstanceRecord = {
6
6
  mesh: AgentMeshParts;
7
7
  visible: boolean;
8
+ opacity?: number;
8
9
  };
9
10
 
10
11
  type EffectBatch = {
@@ -34,13 +35,14 @@ export class AgentEffectInstancedLayer {
34
35
  return;
35
36
  }
36
37
  record.mesh.group.updateMatrixWorld(true);
37
- this.addObject("glow", record.mesh.activityGlow, records.length);
38
+ const opacity = record.opacity ?? 1;
39
+ this.addObject("glow", record.mesh.activityGlow, records.length, opacity);
38
40
  record.mesh.activityEffects.typingDots.forEach((dot) => {
39
- this.addObject("typingDot", dot, records.length * 3);
41
+ this.addObject("typingDot", dot, records.length * 3, opacity);
40
42
  });
41
- this.addObject("toolBlock", record.mesh.activityEffects.toolBlock, records.length);
42
- this.addObject("runningRing", record.mesh.activityEffects.runningRing, records.length);
43
- this.addObject("completionRing", record.mesh.activityEffects.completionRing, records.length);
43
+ this.addObject("toolBlock", record.mesh.activityEffects.toolBlock, records.length, opacity);
44
+ this.addObject("runningRing", record.mesh.activityEffects.runningRing, records.length, opacity);
45
+ this.addObject("completionRing", record.mesh.activityEffects.completionRing, records.length, opacity);
44
46
  });
45
47
 
46
48
  this.batches.forEach((batch) => {
@@ -57,19 +59,20 @@ export class AgentEffectInstancedLayer {
57
59
  this.batches.clear();
58
60
  }
59
61
 
60
- private addObject(key: EffectKey, object: THREE.Object3D, capacity: number): void {
62
+ private addObject(key: EffectKey, object: THREE.Object3D, capacity: number, opacity: number): void {
61
63
  if (!object.visible) {
62
64
  return;
63
65
  }
64
- const batch = this.ensureBatch(key, capacity);
66
+ const batch = this.ensureBatch(key, capacity, opacity);
65
67
  object.updateMatrixWorld(true);
66
68
  matrixHelper.copy(object.matrixWorld);
67
69
  batch.mesh.setMatrixAt(batch.count, matrixHelper);
68
70
  batch.count += 1;
69
71
  }
70
72
 
71
- private ensureBatch(key: EffectKey, requiredCapacity: number): EffectBatch {
72
- const existing = this.batches.get(key);
73
+ private ensureBatch(key: EffectKey, requiredCapacity: number, opacity: number): EffectBatch {
74
+ const batchKey = `${key}:${opacity}`;
75
+ const existing = this.batches.get(batchKey);
73
76
  if (existing && existing.capacity >= requiredCapacity) {
74
77
  return existing;
75
78
  }
@@ -80,21 +83,32 @@ export class AgentEffectInstancedLayer {
80
83
  const spec = effectSpecs[key];
81
84
  const mesh = new THREE.InstancedMesh(
82
85
  spec.geometry,
83
- spec.material,
86
+ resolveEffectMaterial(spec.material, opacity),
84
87
  Math.max(requiredCapacity, 1),
85
88
  );
86
89
  mesh.castShadow = false;
87
90
  mesh.receiveShadow = false;
88
91
  mesh.frustumCulled = false;
89
- mesh.name = `agentEffect:${key}`;
92
+ mesh.name = `agentEffect:${batchKey}`;
90
93
  this.scene.add(mesh);
91
94
 
92
95
  const batch = { mesh, capacity: Math.max(requiredCapacity, 1), count: 0 };
93
- this.batches.set(key, batch);
96
+ this.batches.set(batchKey, batch);
94
97
  return batch;
95
98
  }
96
99
  }
97
100
 
101
+ function resolveEffectMaterial(material: THREE.Material, opacity: number): THREE.Material {
102
+ if (opacity >= 1) {
103
+ return material;
104
+ }
105
+ const clone = material.clone();
106
+ clone.transparent = true;
107
+ clone.opacity *= opacity;
108
+ clone.depthWrite = false;
109
+ return clone;
110
+ }
111
+
98
112
  export function createAgentEffectInstancedLayer(scene: THREE.Scene): AgentEffectInstancedLayer {
99
113
  return new AgentEffectInstancedLayer(scene);
100
114
  }
@@ -2,6 +2,8 @@ import * as THREE from "three";
2
2
 
3
3
  import type { AgentGameOfficeAgent } from "../../core/types";
4
4
 
5
+ export const AGENT_LABEL_WORLD_Y_OFFSET = 3.05;
6
+
5
7
  export type AgentLabelRecord = {
6
8
  root: HTMLDivElement;
7
9
  title: HTMLDivElement;
@@ -69,7 +71,7 @@ export function updateAgentLabelPosition(
69
71
  viewport: HTMLElement,
70
72
  ): void {
71
73
  const projected = position.clone();
72
- projected.y += 2.2;
74
+ projected.y += AGENT_LABEL_WORLD_Y_OFFSET;
73
75
  projected.project(camera);
74
76
  const x = (projected.x * 0.5 + 0.5) * viewport.clientWidth;
75
77
  const y = (-projected.y * 0.5 + 0.5) * viewport.clientHeight;
@@ -13,6 +13,9 @@ export type AgentMeshParts = {
13
13
  head: THREE.Object3D;
14
14
  leftEye: THREE.Object3D;
15
15
  rightEye: THREE.Object3D;
16
+ mouth: THREE.Object3D;
17
+ chestFront: THREE.Object3D;
18
+ backPanel: THREE.Object3D;
16
19
  hairTop: THREE.Object3D;
17
20
  hairBack: THREE.Object3D;
18
21
  leftArm: THREE.Object3D;
@@ -38,6 +41,9 @@ export type AgentBodyPartKey =
38
41
  | "head"
39
42
  | "leftEye"
40
43
  | "rightEye"
44
+ | "mouth"
45
+ | "chestFront"
46
+ | "backPanel"
41
47
  | "hairTop"
42
48
  | "hairBack"
43
49
  | "leftArm"
@@ -45,6 +51,7 @@ export type AgentBodyPartKey =
45
51
 
46
52
  export type AgentBodyPartRenderSpec = {
47
53
  key: AgentBodyPartKey;
54
+ shape?: "box" | "frontPlane" | "backPlane";
48
55
  width: number;
49
56
  height: number;
50
57
  depth: number;
@@ -66,8 +73,11 @@ export function resolveAgentBodyPartRenderSpecs(index: number): AgentBodyPartRen
66
73
  { key: "leftFoot", width: 0.24, height: 0.12, depth: 0.34, color: 0x111827, x: -0.14, y: 0.06, z: 0.08 },
67
74
  { key: "rightFoot", width: 0.24, height: 0.12, depth: 0.34, color: 0x111827, x: 0.14, y: 0.06, z: 0.08 },
68
75
  { key: "head", width: 0.5, height: 0.48, depth: 0.42, color: skin, x: 0, y: 1.48, z: 0 },
69
- { key: "leftEye", width: 0.06, height: 0.07, depth: 0.035, color: 0x111827, x: -0.11, y: 1.5, z: 0.225 },
70
- { key: "rightEye", width: 0.06, height: 0.07, depth: 0.035, color: 0x111827, x: 0.11, y: 1.5, z: 0.225 },
76
+ { key: "leftEye", shape: "frontPlane", width: 0.06, height: 0.07, depth: 0, color: 0x111827, x: -0.11, y: 1.5, z: 0.235 },
77
+ { key: "rightEye", shape: "frontPlane", width: 0.06, height: 0.07, depth: 0, color: 0x111827, x: 0.11, y: 1.5, z: 0.235 },
78
+ { key: "mouth", shape: "frontPlane", width: 0.16, height: 0.035, depth: 0, color: 0x7f1d1d, x: 0, y: 1.38, z: 0.245 },
79
+ { key: "chestFront", shape: "frontPlane", width: 0.28, height: 0.12, depth: 0, color: 0xe0f2fe, x: 0, y: 1.02, z: 0.185 },
80
+ { key: "backPanel", shape: "backPlane", width: 0.36, height: 0.44, depth: 0, color: 0x0f172a, x: 0, y: 0.92, z: -0.185 },
71
81
  { key: "hairTop", width: 0.54, height: 0.14, depth: 0.46, color: hair, x: 0, y: 1.78, z: -0.01 },
72
82
  { key: "hairBack", width: 0.52, height: 0.28, depth: 0.09, color: hair, x: 0, y: 1.61, z: -0.23 },
73
83
  { key: "leftArm", width: 0.15, height: 0.56, depth: 0.17, color: shirt, x: -0.43, y: 0.88, z: 0 },
@@ -99,6 +109,9 @@ export function createAgentMesh(agent: AgentGameOfficeAgent, index: number): Age
99
109
  head: parts.head,
100
110
  leftEye: parts.leftEye,
101
111
  rightEye: parts.rightEye,
112
+ mouth: parts.mouth,
113
+ chestFront: parts.chestFront,
114
+ backPanel: parts.backPanel,
102
115
  hairTop: parts.hairTop,
103
116
  hairBack: parts.hairBack,
104
117
  leftArm: parts.leftArm,
@@ -0,0 +1,220 @@
1
+ import type {
2
+ AgentGameOfficeAgent,
3
+ AgentNavigationError,
4
+ } from "../../core/types";
5
+ import {
6
+ NoBuildingRouteError,
7
+ planAgentRoute,
8
+ type AgentRoute,
9
+ type Vector3Like,
10
+ } from "../../layout";
11
+ import type {
12
+ ResolvedOffstageAnchor,
13
+ ResolvedOfficeLayout,
14
+ ResolvedSeatAnchor,
15
+ } from "../../layout";
16
+
17
+ export type RenderAgentLocationState = {
18
+ currentNodeId: string;
19
+ currentPosition: Vector3Like;
20
+ destinationAnchorId: string | null;
21
+ activeRoute: AgentRoute | null;
22
+ activeStepIndex: number;
23
+ error?: AgentNavigationError;
24
+ };
25
+
26
+ type TargetAnchorResult =
27
+ | { anchor: ResolvedSeatAnchor | ResolvedOffstageAnchor; error?: undefined }
28
+ | { anchor?: undefined; error: AgentNavigationError };
29
+
30
+ export function createAgentRouteState(
31
+ layout: ResolvedOfficeLayout,
32
+ agent: AgentGameOfficeAgent,
33
+ previous: RenderAgentLocationState | undefined,
34
+ ): RenderAgentLocationState {
35
+ if (agent.targetPosition) {
36
+ return {
37
+ currentNodeId: previous?.currentNodeId ?? `explicit:${agent.id}`,
38
+ currentPosition: {
39
+ x: agent.targetPosition.x,
40
+ y: previous?.currentPosition.y ?? 0,
41
+ z: agent.targetPosition.z,
42
+ },
43
+ destinationAnchorId: null,
44
+ activeRoute: null,
45
+ activeStepIndex: 0,
46
+ };
47
+ }
48
+
49
+ const target = resolveAgentRouteTargetAnchor(layout, agent);
50
+ if (target.error) {
51
+ return createErrorState(agent, previous, target.error);
52
+ }
53
+
54
+ const targetNodeId = target.anchor.id;
55
+ const targetPosition = resolveAnchorPosition(layout, target.anchor);
56
+ if (!previous) {
57
+ return {
58
+ currentNodeId: targetNodeId,
59
+ currentPosition: targetPosition,
60
+ destinationAnchorId: targetNodeId,
61
+ activeRoute: null,
62
+ activeStepIndex: 0,
63
+ };
64
+ }
65
+
66
+ if (!layout.building.navigation.nodes.some((node) => node.id === previous.currentNodeId)) {
67
+ return createErrorState(agent, previous, {
68
+ agentId: agent.id,
69
+ code: "stale-location-node",
70
+ message: `Agent ${agent.id} references missing building navigation node ${previous.currentNodeId}`,
71
+ fromNodeId: previous.currentNodeId,
72
+ toNodeId: targetNodeId,
73
+ });
74
+ }
75
+
76
+ if (previous.destinationAnchorId === targetNodeId && !previous.error) {
77
+ return previous;
78
+ }
79
+
80
+ try {
81
+ const route = planAgentRoute(layout.building.navigation, previous.currentNodeId, targetNodeId);
82
+ return {
83
+ currentNodeId: previous.currentNodeId,
84
+ currentPosition: previous.currentPosition,
85
+ destinationAnchorId: targetNodeId,
86
+ activeRoute: route,
87
+ activeStepIndex: 0,
88
+ };
89
+ } catch (error) {
90
+ if (error instanceof NoBuildingRouteError) {
91
+ return createErrorState(agent, previous, {
92
+ agentId: agent.id,
93
+ code: "missing-route",
94
+ message: error.message,
95
+ zoneId: agent.zoneId,
96
+ fromNodeId: previous.currentNodeId,
97
+ toNodeId: targetNodeId,
98
+ });
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ export function resolveAgentRouteTargetAnchor(
105
+ layout: ResolvedOfficeLayout,
106
+ agent: AgentGameOfficeAgent,
107
+ ): TargetAnchorResult {
108
+ if (agent.zoneId === "offstage") {
109
+ return { anchor: layout.anchors.offstage };
110
+ }
111
+
112
+ const floorOrder = new Map(layout.building.floors.map((floor, index) => [floor.id, index]));
113
+ const roomsById = new Map(layout.rooms.map((room) => [room.id, room]));
114
+ const candidates = layout.anchors.seats
115
+ .filter((anchor) => anchor.zoneId === agent.zoneId && anchor.floorId !== "offstage")
116
+ .sort((left, right) => {
117
+ if (agent.zoneId === "meeting-room") {
118
+ const leftIsAuditorium = roomsById.get(left.roomId)?.type === "auditorium";
119
+ const rightIsAuditorium = roomsById.get(right.roomId)?.type === "auditorium";
120
+ if (leftIsAuditorium !== rightIsAuditorium) {
121
+ return leftIsAuditorium ? -1 : 1;
122
+ }
123
+ }
124
+ const floorDelta = (floorOrder.get(left.floorId) ?? 0) - (floorOrder.get(right.floorId) ?? 0);
125
+ if (floorDelta !== 0) {
126
+ return floorDelta;
127
+ }
128
+ return left.id.localeCompare(right.id);
129
+ });
130
+
131
+ const anchor = candidates[0];
132
+ if (!anchor) {
133
+ return {
134
+ error: {
135
+ agentId: agent.id,
136
+ code: "missing-zone-anchor",
137
+ message: `No building navigation anchor for agent ${agent.id} zone ${agent.zoneId}`,
138
+ zoneId: agent.zoneId,
139
+ },
140
+ };
141
+ }
142
+ return { anchor };
143
+ }
144
+
145
+ export function resolveRouteTargetPosition(state: RenderAgentLocationState): Vector3Like {
146
+ const step = state.activeRoute?.steps[state.activeStepIndex];
147
+ return step?.to ?? state.currentPosition;
148
+ }
149
+
150
+ export function advanceAgentRouteState(
151
+ state: RenderAgentLocationState,
152
+ currentPosition: Vector3Like,
153
+ ): RenderAgentLocationState {
154
+ if (!state.activeRoute) {
155
+ return { ...state, currentPosition };
156
+ }
157
+ const step = state.activeRoute.steps[state.activeStepIndex];
158
+ if (!step) {
159
+ return {
160
+ ...state,
161
+ currentPosition,
162
+ currentNodeId: state.activeRoute.toNodeId,
163
+ activeRoute: null,
164
+ activeStepIndex: 0,
165
+ };
166
+ }
167
+ if (state.activeStepIndex < state.activeRoute.steps.length - 1) {
168
+ return {
169
+ ...state,
170
+ currentPosition: step.to,
171
+ currentNodeId: step.toNodeId,
172
+ activeStepIndex: state.activeStepIndex + 1,
173
+ };
174
+ }
175
+ return {
176
+ ...state,
177
+ currentPosition: step.to,
178
+ currentNodeId: step.toNodeId,
179
+ activeRoute: null,
180
+ activeStepIndex: 0,
181
+ };
182
+ }
183
+
184
+ export function resolveAgentRouteFloorId(
185
+ layout: ResolvedOfficeLayout,
186
+ state: RenderAgentLocationState,
187
+ ): string | null {
188
+ const node = layout.building.navigation.nodes.find((item) => item.id === state.currentNodeId);
189
+ return node?.floorId ?? null;
190
+ }
191
+
192
+ function createErrorState(
193
+ agent: AgentGameOfficeAgent,
194
+ previous: RenderAgentLocationState | undefined,
195
+ error: AgentNavigationError,
196
+ ): RenderAgentLocationState {
197
+ return {
198
+ currentNodeId: previous?.currentNodeId ?? "offstage",
199
+ currentPosition: previous?.currentPosition ?? { x: 0, y: 0, z: 0 },
200
+ destinationAnchorId: previous?.destinationAnchorId ?? null,
201
+ activeRoute: null,
202
+ activeStepIndex: 0,
203
+ error: { ...error, agentId: agent.id },
204
+ };
205
+ }
206
+
207
+ function resolveAnchorPosition(
208
+ layout: ResolvedOfficeLayout,
209
+ anchor: ResolvedSeatAnchor | ResolvedOffstageAnchor,
210
+ ): Vector3Like {
211
+ if (anchor.floorId === null) {
212
+ return anchor.position;
213
+ }
214
+ const floor = layout.building.floors.find((item) => item.id === anchor.floorId);
215
+ return {
216
+ x: anchor.position.x,
217
+ y: floor?.elevation ?? 0,
218
+ z: anchor.position.z,
219
+ };
220
+ }