let-them-talk 5.3.0 → 5.4.0

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.
Files changed (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
@@ -0,0 +1,997 @@
1
+ import * as THREE from 'three';
2
+ import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
+ import { S } from './state.js';
4
+
5
+ // ============================================================
6
+ // GALLERY WING — Premium art gallery inside the campus
7
+ // Far-left upper zone, open east side facing workspace
8
+ // Houses API agent robot + premium display screens
9
+ // ============================================================
10
+
11
+ // Position: inside campus, upper-left area
12
+ var GALLERY_X = -36; // gallery center X
13
+ var GALLERY_Z = 10; // gallery center Z
14
+ var GALLERY_W = 14; // gallery width (X axis)
15
+ var GALLERY_D = 12; // gallery depth (Z axis)
16
+ var GALLERY_H = 5.5; // gallery height (taller for drama)
17
+ var SCREEN_W = 4.5; // large premium screens
18
+ var SCREEN_H = 2.8;
19
+
20
+ // Robot desk — 3 seats, one per monitor (image/video/texture)
21
+ var _deskCenter = { x: GALLERY_X - 2, z: GALLERY_Z + 2 };
22
+ export var GALLERY_DESK_POS = _deskCenter; // legacy compat
23
+ export var GALLERY_SEATS = {
24
+ image: { x: _deskCenter.x - 0.9, z: _deskCenter.z + 0.8 }, // left monitor
25
+ video: { x: _deskCenter.x, z: _deskCenter.z + 0.8 }, // center monitor
26
+ texture: { x: _deskCenter.x + 0.9, z: _deskCenter.z + 0.8 }, // right monitor
27
+ };
28
+
29
+ export function buildGalleryRoom() {
30
+ var group = new THREE.Group();
31
+
32
+ // === MATERIALS PALETTE (premium) ===
33
+ var wallMat = new THREE.MeshStandardMaterial({ color: 0x12121e, roughness: 0.85 });
34
+ var accentWallMat = new THREE.MeshStandardMaterial({ color: 0x0e0e1a, roughness: 0.8 });
35
+ var glassMat = new THREE.MeshStandardMaterial({ color: 0xaaccee, transparent: true, opacity: 0.18, roughness: 0.05, metalness: 0.15 });
36
+ var glassFrameMat = new THREE.MeshStandardMaterial({ color: 0x555566, roughness: 0.2, metalness: 0.6 });
37
+ var chromeMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.1, metalness: 0.85 });
38
+ var goldMat = new THREE.MeshStandardMaterial({ color: 0xd4af37, roughness: 0.3, metalness: 0.7 });
39
+ var darkMat = new THREE.MeshStandardMaterial({ color: 0x0a0a14, roughness: 0.3, metalness: 0.1 });
40
+ var neonCyanMat = new THREE.MeshStandardMaterial({ color: 0x06b6d4, emissive: 0x06b6d4, emissiveIntensity: 0.7, roughness: 0.2 });
41
+ var neonPinkMat = new THREE.MeshStandardMaterial({ color: 0xec4899, emissive: 0xec4899, emissiveIntensity: 0.6, roughness: 0.2 });
42
+ var neonGreenMat = new THREE.MeshStandardMaterial({ color: 0x22c55e, emissive: 0x22c55e, emissiveIntensity: 0.6, roughness: 0.2 });
43
+ var leatherMat = new THREE.MeshStandardMaterial({ color: 0x1a1a22, roughness: 0.65 });
44
+
45
+ // ============================================================
46
+ // GALLERY ROOM (inside campus, open east side)
47
+ // ============================================================
48
+ var gx = GALLERY_X, gz = GALLERY_Z;
49
+ var hw = GALLERY_W / 2, hd = GALLERY_D / 2;
50
+
51
+ // --- FLOOR: Premium dark polished concrete with gold veins ---
52
+ var floorSize = 512;
53
+ var floorCvs = document.createElement('canvas');
54
+ floorCvs.width = floorSize; floorCvs.height = floorSize;
55
+ var floorCtx = floorCvs.getContext('2d');
56
+ // Dark base
57
+ floorCtx.fillStyle = '#0c0c16';
58
+ floorCtx.fillRect(0, 0, floorSize, floorSize);
59
+ // Subtle gold veins
60
+ for (var vi = 0; vi < 40; vi++) {
61
+ floorCtx.beginPath();
62
+ floorCtx.strokeStyle = 'rgba(180,150,80,' + (0.03 + Math.random() * 0.06) + ')';
63
+ floorCtx.lineWidth = 0.5 + Math.random() * 1.5;
64
+ var sx = Math.random() * floorSize, sy = Math.random() * floorSize;
65
+ floorCtx.moveTo(sx, sy);
66
+ for (var vj = 0; vj < 5; vj++) {
67
+ sx += (Math.random() - 0.5) * 100;
68
+ sy += (Math.random() - 0.5) * 100;
69
+ floorCtx.lineTo(sx, sy);
70
+ }
71
+ floorCtx.stroke();
72
+ }
73
+ // Tile grid
74
+ var tileSize = floorSize / 8;
75
+ floorCtx.strokeStyle = 'rgba(40,40,60,0.5)';
76
+ floorCtx.lineWidth = 1;
77
+ for (var tx = 0; tx <= 8; tx++) {
78
+ floorCtx.beginPath(); floorCtx.moveTo(tx * tileSize, 0); floorCtx.lineTo(tx * tileSize, floorSize); floorCtx.stroke();
79
+ floorCtx.beginPath(); floorCtx.moveTo(0, tx * tileSize); floorCtx.lineTo(floorSize, tx * tileSize); floorCtx.stroke();
80
+ }
81
+
82
+ var floorTex = new THREE.CanvasTexture(floorCvs);
83
+ floorTex.wrapS = floorTex.wrapT = THREE.RepeatWrapping;
84
+ var floor = new THREE.Mesh(new THREE.PlaneGeometry(GALLERY_W, GALLERY_D),
85
+ new THREE.MeshStandardMaterial({ map: floorTex, roughness: 0.1, metalness: 0.08 }));
86
+ floor.rotation.x = -Math.PI / 2;
87
+ floor.position.set(gx, 0.02, gz);
88
+ floor.receiveShadow = true;
89
+ group.add(floor);
90
+
91
+ // --- WALLS ---
92
+ // West wall — IMAGE GALLERY (main showcase, deepest wall)
93
+ var westWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, GALLERY_H, GALLERY_D), accentWallMat);
94
+ westWall.position.set(gx - hw, GALLERY_H / 2, gz);
95
+ westWall.castShadow = true;
96
+ group.add(westWall);
97
+
98
+ // South wall (VIDEO GALLERY)
99
+ var southWall = new THREE.Mesh(new THREE.BoxGeometry(GALLERY_W, GALLERY_H, 0.2), wallMat);
100
+ southWall.position.set(gx, GALLERY_H / 2, gz - hd);
101
+ southWall.castShadow = true;
102
+ group.add(southWall);
103
+
104
+ // North wall (TEXTURE GALLERY)
105
+ var northWall = new THREE.Mesh(new THREE.BoxGeometry(GALLERY_W, GALLERY_H, 0.2), wallMat);
106
+ northWall.position.set(gx, GALLERY_H / 2, gz + hd);
107
+ northWall.castShadow = true;
108
+ group.add(northWall);
109
+
110
+ // East side — glass facade with center entrance (faces workspace)
111
+ var doorWidth = 2.5;
112
+ var eastPanelD = (GALLERY_D - doorWidth) / 2;
113
+ // South glass panel
114
+ var egSouth = new THREE.Mesh(new THREE.BoxGeometry(0.08, GALLERY_H, eastPanelD), glassMat);
115
+ egSouth.position.set(gx + hw, GALLERY_H / 2, gz - doorWidth / 2 - eastPanelD / 2);
116
+ group.add(egSouth);
117
+ // North glass panel
118
+ var egNorth = new THREE.Mesh(new THREE.BoxGeometry(0.08, GALLERY_H, eastPanelD), glassMat);
119
+ egNorth.position.set(gx + hw, GALLERY_H / 2, gz + doorWidth / 2 + eastPanelD / 2);
120
+ group.add(egNorth);
121
+ // Door header
122
+ var doorHeader = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.12, doorWidth + 0.4), chromeMat);
123
+ doorHeader.position.set(gx + hw, GALLERY_H - 0.4, gz);
124
+ group.add(doorHeader);
125
+ // Glass frame mullions (vertical chrome strips)
126
+ [-eastPanelD, 0, eastPanelD].forEach(function(mz) {
127
+ [-1, 1].forEach(function(side) {
128
+ if (mz === 0 && side === -1) return;
129
+ var mullion = new THREE.Mesh(new THREE.BoxGeometry(0.1, GALLERY_H, 0.03), glassFrameMat);
130
+ mullion.position.set(gx + hw, GALLERY_H / 2, gz + mz * side);
131
+ group.add(mullion);
132
+ });
133
+ });
134
+
135
+ // Ceiling — dark with recessed lighting channels
136
+ var ceiling = new THREE.Mesh(new THREE.PlaneGeometry(GALLERY_W, GALLERY_D), wallMat);
137
+ ceiling.rotation.x = Math.PI / 2;
138
+ ceiling.position.set(gx, GALLERY_H, gz);
139
+ group.add(ceiling);
140
+
141
+ // --- GOLD ACCENT TRIM (baseboard + crown molding) ---
142
+ // Baseboard: west, south, north walls
143
+ [[-hw, 0.04, 0, 0.06, 0.08, GALLERY_D],
144
+ [0, 0.04, -hd, GALLERY_W, 0.08, 0.06],
145
+ [0, 0.04, hd, GALLERY_W, 0.08, 0.06]].forEach(function(t) {
146
+ var trim = new THREE.Mesh(new THREE.BoxGeometry(t[3], t[4], t[5]), goldMat);
147
+ trim.position.set(gx + t[0], t[1], gz + t[2]);
148
+ group.add(trim);
149
+ });
150
+ // Crown molding: west, south, north walls
151
+ [[-hw, GALLERY_H - 0.04, 0, 0.08, 0.06, GALLERY_D],
152
+ [0, GALLERY_H - 0.04, -hd, GALLERY_W + 0.1, 0.06, 0.08],
153
+ [0, GALLERY_H - 0.04, hd, GALLERY_W + 0.1, 0.06, 0.08]].forEach(function(t) {
154
+ var crown = new THREE.Mesh(new THREE.BoxGeometry(t[3], t[4], t[5]), goldMat);
155
+ crown.position.set(gx + t[0], t[1], gz + t[2]);
156
+ group.add(crown);
157
+ });
158
+
159
+ // ============================================================
160
+ // SCREENS — Large premium displays
161
+ // ============================================================
162
+
163
+ // IMAGE GALLERY (west wall — centerpiece, biggest screen)
164
+ var imgScreen = buildScreen(SCREEN_W, SCREEN_H, 'IMAGE GALLERY', neonCyanMat, 0x06b6d4, 1280, 800);
165
+ imgScreen.group.position.set(gx - hw + 0.15, GALLERY_H / 2 - 0.25, gz);
166
+ imgScreen.group.rotation.y = Math.PI / 2;
167
+ group.add(imgScreen.group);
168
+
169
+ // VIDEO GALLERY (south wall)
170
+ var vidScreen = buildScreen(SCREEN_W - 0.5, SCREEN_H - 0.3, 'VIDEO GALLERY', neonPinkMat, 0xec4899, 1280, 800);
171
+ vidScreen.group.position.set(gx, GALLERY_H / 2 - 0.25, gz - hd + 0.15);
172
+ group.add(vidScreen.group);
173
+
174
+ // TEXTURE GALLERY (north wall)
175
+ var texScreen = buildScreen(SCREEN_W - 0.5, SCREEN_H - 0.3, 'TEXTURE GALLERY', neonGreenMat, 0x22c55e, 1280, 800);
176
+ texScreen.group.position.set(gx, GALLERY_H / 2 - 0.25, gz + hd - 0.15);
177
+ texScreen.group.rotation.y = Math.PI;
178
+ group.add(texScreen.group);
179
+
180
+ // ============================================================
181
+ // TRACK LIGHTING (museum-style spotlights)
182
+ // ============================================================
183
+ // West wall spots (illuminate image gallery — main showcase)
184
+ [-2.5, 0, 2.5].forEach(function(lz) {
185
+ var spot = new THREE.SpotLight(0xeeeeff, 0.6, 8, Math.PI / 6, 0.4);
186
+ spot.position.set(gx - hw + 2.5, GALLERY_H - 0.3, gz + lz);
187
+ spot.target.position.set(gx - hw + 0.2, GALLERY_H / 2, gz + lz);
188
+ group.add(spot);
189
+ group.add(spot.target);
190
+ var rail = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.04, 0.04), chromeMat);
191
+ rail.position.set(gx - hw + 2.5, GALLERY_H - 0.15, gz + lz);
192
+ group.add(rail);
193
+ var housing = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.04, 0.1, 8), darkMat);
194
+ housing.position.set(gx - hw + 2.5, GALLERY_H - 0.25, gz + lz);
195
+ group.add(housing);
196
+ });
197
+
198
+ // South + north wall spots
199
+ [[gz - hd + 2, gz - hd + 0.2], [gz + hd - 2, gz + hd - 0.2]].forEach(function(sp) {
200
+ var spot = new THREE.SpotLight(0xeeeeff, 0.4, 7, Math.PI / 6, 0.4);
201
+ spot.position.set(gx, GALLERY_H - 0.3, sp[0]);
202
+ spot.target.position.set(gx, GALLERY_H / 2, sp[1]);
203
+ group.add(spot);
204
+ group.add(spot.target);
205
+ });
206
+
207
+ // Main overhead track rail (chrome bar along Z axis near west wall)
208
+ var mainRail = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, GALLERY_D - 2), chromeMat);
209
+ mainRail.position.set(gx - hw + 2.5, GALLERY_H - 0.08, gz);
210
+ group.add(mainRail);
211
+
212
+ // Ambient fill light
213
+ var ambientFill = new THREE.PointLight(0x223344, 0.25, 14);
214
+ ambientFill.position.set(gx, GALLERY_H - 0.5, gz);
215
+ group.add(ambientFill);
216
+
217
+ // ============================================================
218
+ // VIEWING FURNITURE
219
+ // ============================================================
220
+
221
+ // Premium leather bench (center, facing back wall)
222
+ var benchSeat = new THREE.Mesh(new THREE.BoxGeometry(3, 0.1, 0.7), leatherMat);
223
+ benchSeat.position.set(gx, 0.48, gz + 0.5);
224
+ benchSeat.castShadow = true;
225
+ group.add(benchSeat);
226
+ // Bench chrome legs
227
+ [[-1.3, -0.25], [-1.3, 0.25], [1.3, -0.25], [1.3, 0.25]].forEach(function(p) {
228
+ var leg = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.45, 8), chromeMat);
229
+ leg.position.set(gx + p[0], 0.24, gz + 0.5 + p[1]);
230
+ group.add(leg);
231
+ });
232
+
233
+ // Side tables (small chrome + glass)
234
+ [-2.5, 2.5].forEach(function(stx) {
235
+ var tableTop = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, 0.03, 12),
236
+ new THREE.MeshStandardMaterial({ color: 0x222233, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.4 }));
237
+ tableTop.position.set(gx + stx, 0.5, gz + 0.5);
238
+ group.add(tableTop);
239
+ var tableLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.48, 8), chromeMat);
240
+ tableLeg.position.set(gx + stx, 0.26, gz + 0.5);
241
+ group.add(tableLeg);
242
+ var tableBase = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.15, 0.02, 12), chromeMat);
243
+ tableBase.position.set(gx + stx, 0.02, gz + 0.5);
244
+ group.add(tableBase);
245
+ });
246
+
247
+ // ============================================================
248
+ // ROBOT WORKSTATION (left-front area of gallery)
249
+ // ============================================================
250
+ var deskX = GALLERY_DESK_POS.x, deskZ = GALLERY_DESK_POS.z;
251
+
252
+ // Server-style desk (wider, with multiple monitors)
253
+ var deskMat = new THREE.MeshStandardMaterial({ color: 0x111118, roughness: 0.3, metalness: 0.15 });
254
+ var deskTop = new THREE.Mesh(new THREE.BoxGeometry(3.2, 0.05, 1), deskMat);
255
+ deskTop.position.set(deskX, 0.76, deskZ);
256
+ deskTop.castShadow = true;
257
+ group.add(deskTop);
258
+
259
+ // RGB LED under desk edge (cyan for Ollama default)
260
+ var deskLed = new THREE.Mesh(new THREE.BoxGeometry(3.1, 0.015, 0.015), neonCyanMat);
261
+ deskLed.position.set(deskX, 0.74, deskZ + 0.49);
262
+ group.add(deskLed);
263
+
264
+ // Desk legs (chrome)
265
+ [[-1.5, -0.4], [-1.5, 0.4], [1.5, -0.4], [1.5, 0.4]].forEach(function(p) {
266
+ var leg = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.76, 0.05), chromeMat);
267
+ leg.position.set(deskX + p[0], 0.38, deskZ + p[1]);
268
+ group.add(leg);
269
+ });
270
+
271
+ // Triple monitor setup — canvas-textured, mirroring wall screens
272
+ var deskMonitorOffsets = [-0.9, 0, 0.9];
273
+ var deskMonitorLabels = ['IMAGE GALLERY', 'VIDEO GALLERY', 'TEXTURE GALLERY'];
274
+ var deskMonitorColors = [0x06b6d4, 0xec4899, 0x22c55e];
275
+ var deskMonitorKeys = ['image', 'video', 'texture'];
276
+ S.galleryDeskScreens = {};
277
+
278
+ deskMonitorOffsets.forEach(function(mx, mi) {
279
+ var monBody = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.32, 0.025),
280
+ new THREE.MeshStandardMaterial({ color: 0x080810, roughness: 0.2 }));
281
+ monBody.position.set(deskX + mx, 1.15, deskZ - 0.3);
282
+ if (mi === 0) monBody.rotation.y = 0.12;
283
+ if (mi === 2) monBody.rotation.y = -0.12;
284
+ monBody.castShadow = true;
285
+ group.add(monBody);
286
+
287
+ // Canvas-textured monitor screen
288
+ var monCvs = document.createElement('canvas');
289
+ var monCW = 640, monCH = 400;
290
+ monCvs.width = monCW; monCvs.height = monCH;
291
+ var monCtx = monCvs.getContext('2d');
292
+ // Premium placeholder — same style as buildScreen()
293
+ monCtx.fillStyle = '#08080f';
294
+ monCtx.fillRect(0, 0, monCW, monCH);
295
+ monCtx.strokeStyle = 'rgba(40,40,60,0.3)';
296
+ monCtx.lineWidth = 0.5;
297
+ for (var mgx = 0; mgx < monCW; mgx += 40) {
298
+ monCtx.beginPath(); monCtx.moveTo(mgx, 0); monCtx.lineTo(mgx, monCH); monCtx.stroke();
299
+ }
300
+ for (var mgy = 0; mgy < monCH; mgy += 40) {
301
+ monCtx.beginPath(); monCtx.moveTo(0, mgy); monCtx.lineTo(monCW, mgy); monCtx.stroke();
302
+ }
303
+ var monColorHex = '#' + deskMonitorColors[mi].toString(16).padStart(6, '0');
304
+ monCtx.fillStyle = monColorHex;
305
+ monCtx.globalAlpha = 0.4;
306
+ monCtx.font = 'bold 28px monospace';
307
+ monCtx.textAlign = 'center';
308
+ monCtx.fillText(deskMonitorLabels[mi], monCW / 2, monCH / 2 - 10);
309
+ monCtx.globalAlpha = 0.25;
310
+ monCtx.font = '14px monospace';
311
+ monCtx.fillText('Awaiting content...', monCW / 2, monCH / 2 + 22);
312
+ monCtx.globalAlpha = 1;
313
+
314
+ var monTex = new THREE.CanvasTexture(monCvs);
315
+ monTex.minFilter = THREE.LinearFilter;
316
+ var monScreenMat = new THREE.MeshStandardMaterial({
317
+ map: monTex, emissive: 0xffffff, emissiveMap: monTex, emissiveIntensity: 0.9, roughness: 0.3, metalness: 0,
318
+ });
319
+ var monScreen = new THREE.Mesh(new THREE.PlaneGeometry(0.54, 0.27), monScreenMat);
320
+ monScreen.position.set(deskX + mx, 1.15, deskZ - 0.286);
321
+ if (mi === 0) { monScreen.rotation.y = 0.12; monScreen.position.x += 0.01; monScreen.position.z += 0.005; }
322
+ if (mi === 2) { monScreen.rotation.y = -0.12; monScreen.position.x -= 0.01; monScreen.position.z += 0.005; }
323
+ group.add(monScreen);
324
+
325
+ // Monitor stand
326
+ var standArm = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.2, 0.03), chromeMat);
327
+ standArm.position.set(deskX + mx, 0.92, deskZ - 0.3);
328
+ group.add(standArm);
329
+
330
+ // Store reference in S.galleryDeskScreens
331
+ S.galleryDeskScreens[deskMonitorKeys[mi]] = {
332
+ canvas: monCvs, context: monCtx, texture: monTex, canvasW: monCW, canvasH: monCH,
333
+ };
334
+ });
335
+
336
+ // Gaming keyboard
337
+ var kbMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.5, metalness: 0.2 });
338
+ var keyboard = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.02, 0.12), kbMat);
339
+ keyboard.position.set(deskX - 0.1, 0.78, deskZ + 0.15);
340
+ keyboard.castShadow = true;
341
+ group.add(keyboard);
342
+
343
+ // Mousepad
344
+ var padMat = new THREE.MeshStandardMaterial({ color: 0x0d0d0d, roughness: 0.9, metalness: 0.0 });
345
+ var mousepad = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.005, 0.20), padMat);
346
+ mousepad.position.set(deskX + 0.3, 0.765, deskZ + 0.15);
347
+ group.add(mousepad);
348
+
349
+ // Mouse
350
+ var mouseMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2a, roughness: 0.4, metalness: 0.3 });
351
+ var mouse = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.02, 0.07), mouseMat);
352
+ mouse.position.set(deskX + 0.3, 0.78, deskZ + 0.15);
353
+ mouse.castShadow = true;
354
+ group.add(mouse);
355
+
356
+ // Gaming PC tower
357
+ var towerMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.35, metalness: 0.25 });
358
+ var tower = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.45, 0.45), towerMat);
359
+ tower.position.set(deskX + 1.0, 0.23, deskZ);
360
+ tower.castShadow = true;
361
+ group.add(tower);
362
+ // RGB glass side panel (cyan glow)
363
+ var rgbGlassMat = new THREE.MeshStandardMaterial({
364
+ color: 0x06b6d4, emissive: 0x06b6d4, emissiveIntensity: 0.45,
365
+ transparent: true, opacity: 0.35, roughness: 0.05, metalness: 0.1,
366
+ });
367
+ var rgbPanel = new THREE.Mesh(new THREE.PlaneGeometry(0.42, 0.42), rgbGlassMat);
368
+ rgbPanel.rotation.y = -Math.PI / 2;
369
+ rgbPanel.position.set(deskX + 1.0 + 0.111, 0.23, deskZ);
370
+ group.add(rgbPanel);
371
+
372
+ // Server rack (behind desk, small decorative)
373
+ var rackMat = new THREE.MeshStandardMaterial({ color: 0x111118, roughness: 0.4, metalness: 0.3 });
374
+ var rack = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.2, 0.4), rackMat);
375
+ rack.position.set(deskX + 1.5, 0.6, deskZ - 0.3);
376
+ rack.castShadow = true;
377
+ group.add(rack);
378
+ // Rack LED indicators
379
+ for (var ri = 0; ri < 4; ri++) {
380
+ var ledColor = [0x22c55e, 0x06b6d4, 0x22c55e, 0xeab308][ri];
381
+ var rackLed = new THREE.Mesh(new THREE.SphereGeometry(0.015, 6, 6),
382
+ new THREE.MeshStandardMaterial({ color: ledColor, emissive: ledColor, emissiveIntensity: 0.8 }));
383
+ rackLed.position.set(deskX + 1.5 + 0.31, 0.35 + ri * 0.22, deskZ - 0.1);
384
+ group.add(rackLed);
385
+ }
386
+
387
+ // 3 Gaming chairs — one per monitor seat (image/video/texture)
388
+ buildGalleryChair(group, deskX - 0.9, deskZ + 0.8, 0x06b6d4); // left = image (cyan)
389
+ buildGalleryChair(group, deskX, deskZ + 0.8, 0xec4899); // center = video (pink)
390
+ buildGalleryChair(group, deskX + 0.9, deskZ + 0.8, 0x22c55e); // right = texture (green)
391
+
392
+ // ============================================================
393
+ // NEON ACCENTS & SIGNS
394
+ // ============================================================
395
+
396
+ // "GALLERY" sign (above entrance, exterior facing)
397
+ var signDiv = document.createElement('div');
398
+ signDiv.textContent = 'G A L L E R Y';
399
+ signDiv.style.cssText = 'color:#06b6d4;font-size:16px;font-weight:900;font-family:Inter,sans-serif;letter-spacing:8px;text-shadow:0 0 20px rgba(6,182,212,0.7),0 0 40px rgba(6,182,212,0.3);';
400
+ var signLabel = new CSS2DObject(signDiv);
401
+ signLabel.position.set(gx + hw + 0.3, GALLERY_H + 0.3, gz);
402
+ group.add(signLabel);
403
+
404
+ // Interior neon accent strips along ceiling edges
405
+ [[-hw + 0.1, GALLERY_H - 0.06, 0, 0.02, 0.02, GALLERY_D - 0.4, neonCyanMat],
406
+ [0, GALLERY_H - 0.06, -hd + 0.1, GALLERY_W - 0.4, 0.02, 0.02, neonPinkMat],
407
+ [0, GALLERY_H - 0.06, hd - 0.1, GALLERY_W - 0.4, 0.02, 0.02, neonGreenMat]].forEach(function(n) {
408
+ var strip = new THREE.Mesh(new THREE.BoxGeometry(n[3], n[4], n[5]), n[6]);
409
+ strip.position.set(gx + n[0], n[1], gz + n[2]);
410
+ group.add(strip);
411
+ });
412
+
413
+ // Floor LED guide strips (leading to each screen)
414
+ // West wall guide (to image gallery)
415
+ var guideWest = new THREE.Mesh(new THREE.BoxGeometry(hw - 1, 0.01, 0.03), neonCyanMat);
416
+ guideWest.position.set(gx - (hw - 1) / 2 - 0.5, 0.025, gz);
417
+ group.add(guideWest);
418
+ // South wall guide (to video gallery)
419
+ var guideSouth = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.01, hd - 2), neonPinkMat);
420
+ guideSouth.position.set(gx, 0.025, gz - (hd - 2) / 2 - 1);
421
+ group.add(guideSouth);
422
+ // North wall guide (to texture gallery)
423
+ var guideNorth = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.01, hd - 2), neonGreenMat);
424
+ guideNorth.position.set(gx, 0.025, gz + (hd - 2) / 2 + 1);
425
+ group.add(guideNorth);
426
+
427
+ // ============================================================
428
+ // DECORATIVE ELEMENTS
429
+ // ============================================================
430
+
431
+ // Sculptural pedestal with abstract art (near entrance)
432
+ var pedestalMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.3, metalness: 0.1 });
433
+ var pedestal = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.5), pedestalMat);
434
+ pedestal.position.set(gx + 3, 0.5, gz + 4);
435
+ pedestal.castShadow = true;
436
+ group.add(pedestal);
437
+ // Gold top plate
438
+ var pedTop = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.03, 0.55), goldMat);
439
+ pedTop.position.set(gx + 3, 1.015, gz + 4);
440
+ group.add(pedTop);
441
+ // Abstract sphere sculpture
442
+ var sculptMat = new THREE.MeshStandardMaterial({ color: 0x06b6d4, roughness: 0.05, metalness: 0.9 });
443
+ var sculpt = new THREE.Mesh(new THREE.SphereGeometry(0.2, 16, 16), sculptMat);
444
+ sculpt.position.set(gx + 3, 1.25, gz + 4);
445
+ group.add(sculpt);
446
+
447
+ // Second pedestal (left side)
448
+ var pedestal2 = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.5), pedestalMat);
449
+ pedestal2.position.set(gx - 3, 0.5, gz + 4);
450
+ pedestal2.castShadow = true;
451
+ group.add(pedestal2);
452
+ var pedTop2 = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.03, 0.55), goldMat);
453
+ pedTop2.position.set(gx - 3, 1.015, gz + 4);
454
+ group.add(pedTop2);
455
+ // Abstract cube sculpture (rotated)
456
+ var sculptMat2 = new THREE.MeshStandardMaterial({ color: 0xec4899, roughness: 0.05, metalness: 0.9 });
457
+ var sculpt2 = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.28, 0.28), sculptMat2);
458
+ sculpt2.position.set(gx - 3, 1.2, gz + 4);
459
+ sculpt2.rotation.y = Math.PI / 4;
460
+ sculpt2.rotation.x = Math.PI / 6;
461
+ group.add(sculpt2);
462
+
463
+ // Luxury plant (near entrance right)
464
+ var potMat = new THREE.MeshStandardMaterial({ color: 0x2a2a3a, roughness: 0.5 });
465
+ var pot = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.14, 0.35, 10), potMat);
466
+ pot.position.set(gx + 5.5, 0.18, gz + 4.5);
467
+ group.add(pot);
468
+ var leafMat = new THREE.MeshStandardMaterial({ color: 0x2d5a27, roughness: 0.7 });
469
+ var leaves = new THREE.Mesh(new THREE.SphereGeometry(0.35, 8, 8), leafMat);
470
+ leaves.position.set(gx + 5.5, 0.6, gz + 4.5);
471
+ leaves.scale.y = 1.3;
472
+ group.add(leaves);
473
+
474
+ S.furnitureGroup.add(group);
475
+
476
+ // Store screen references for updates
477
+ S.galleryScreens = {
478
+ image: imgScreen,
479
+ video: vidScreen,
480
+ texture: texScreen,
481
+ };
482
+ // Tag screen meshes for raycast identification
483
+ if (imgScreen.screenMesh) imgScreen.screenMesh.userData._galleryScreen = 'image';
484
+ if (vidScreen.screenMesh) vidScreen.screenMesh.userData._galleryScreen = 'video';
485
+ if (texScreen.screenMesh) texScreen.screenMesh.userData._galleryScreen = 'texture';
486
+ // Collect all screen meshes for easy raycasting
487
+ S._galleryScreenMeshes = [imgScreen.screenMesh, vidScreen.screenMesh, texScreen.screenMesh].filter(Boolean);
488
+ S.cachedMedia = [];
489
+ S._galleryDeskPos = GALLERY_DESK_POS;
490
+ S._gallerySeats = GALLERY_SEATS;
491
+
492
+ return group;
493
+ }
494
+
495
+ function buildGalleryChair(parent, cx, cz, accentColor) {
496
+ // Chair faces -Z (toward monitors at deskZ - 0.3)
497
+ // Backrest goes behind the bot at +Z side
498
+ var baseMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.4, metalness: 0.3 });
499
+ var seatMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.65 });
500
+ var accentMat = new THREE.MeshStandardMaterial({ color: accentColor, roughness: 0.5 });
501
+
502
+ // Base hub
503
+ var baseHub = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.04, 12), baseMat);
504
+ baseHub.position.set(cx, 0.05, cz);
505
+ parent.add(baseHub);
506
+ // Star base arms + wheels
507
+ for (var i = 0; i < 5; i++) {
508
+ var a = (i / 5) * Math.PI * 2;
509
+ var arm = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.02, 0.03), baseMat);
510
+ arm.position.set(cx + Math.cos(a) * 0.15, 0.04, cz + Math.sin(a) * 0.15);
511
+ arm.rotation.y = -a;
512
+ parent.add(arm);
513
+ var wheel = new THREE.Mesh(new THREE.SphereGeometry(0.02, 6, 4), baseMat);
514
+ wheel.position.set(cx + Math.cos(a) * 0.28, 0.02, cz + Math.sin(a) * 0.28);
515
+ parent.add(wheel);
516
+ }
517
+ // Stem
518
+ var stem = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.35, 8), baseMat);
519
+ stem.position.set(cx, 0.22, cz);
520
+ parent.add(stem);
521
+ // Seat (matches campus chair height Y=0.46)
522
+ var seat = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.08, 0.42), seatMat);
523
+ seat.position.set(cx, 0.46, cz);
524
+ seat.castShadow = true;
525
+ parent.add(seat);
526
+ // Backrest — BEHIND the bot (+Z side, bot faces -Z toward monitors)
527
+ var back = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.55, 0.06), seatMat);
528
+ back.position.set(cx, 0.78, cz + 0.20);
529
+ back.castShadow = true;
530
+ parent.add(back);
531
+ // Headrest
532
+ var headrest = new THREE.Mesh(new THREE.BoxGeometry(0.20, 0.10, 0.06), seatMat);
533
+ headrest.position.set(cx, 1.10, cz + 0.20);
534
+ parent.add(headrest);
535
+ // Accent stripes on backrest (face -Z, visible side)
536
+ var s1 = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.50, 0.005), accentMat);
537
+ s1.position.set(cx - 0.12, 0.78, cz + 0.17);
538
+ parent.add(s1);
539
+ var s2 = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.50, 0.005), accentMat);
540
+ s2.position.set(cx + 0.12, 0.78, cz + 0.17);
541
+ parent.add(s2);
542
+ // Armrests
543
+ [-0.22, 0.22].forEach(function(ax) {
544
+ var ap = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.20, 0.03), baseMat);
545
+ ap.position.set(cx + ax, 0.55, cz + 0.05);
546
+ parent.add(ap);
547
+ var apd = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.02, 0.20), seatMat);
548
+ apd.position.set(cx + ax, 0.66, cz + 0.05);
549
+ parent.add(apd);
550
+ });
551
+ }
552
+
553
+ function buildScreen(w, h, label, neonMat, neonColor, canvasW, canvasH) {
554
+ var group = new THREE.Group();
555
+ canvasW = canvasW || 1280;
556
+ canvasH = canvasH || 800;
557
+
558
+ // Outer frame (thick, premium)
559
+ var frame = new THREE.Mesh(new THREE.BoxGeometry(w + 0.2, h + 0.2, 0.06),
560
+ new THREE.MeshStandardMaterial({ color: 0x080810, roughness: 0.15, metalness: 0.3 }));
561
+ group.add(frame);
562
+
563
+ // Inner bezel (thin chrome)
564
+ var bezel = new THREE.Mesh(new THREE.BoxGeometry(w + 0.08, h + 0.08, 0.02),
565
+ new THREE.MeshStandardMaterial({ color: 0x444455, roughness: 0.1, metalness: 0.7 }));
566
+ bezel.position.z = 0.02;
567
+ group.add(bezel);
568
+
569
+ // Canvas screen
570
+ var cvs = document.createElement('canvas');
571
+ cvs.width = canvasW;
572
+ cvs.height = canvasH;
573
+ var ctx = cvs.getContext('2d');
574
+
575
+ // Premium placeholder
576
+ ctx.fillStyle = '#08080f';
577
+ ctx.fillRect(0, 0, canvasW, canvasH);
578
+ // Subtle grid pattern
579
+ ctx.strokeStyle = 'rgba(40,40,60,0.3)';
580
+ ctx.lineWidth = 0.5;
581
+ for (var gx = 0; gx < canvasW; gx += 40) {
582
+ ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, canvasH); ctx.stroke();
583
+ }
584
+ for (var gy = 0; gy < canvasH; gy += 40) {
585
+ ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(canvasW, gy); ctx.stroke();
586
+ }
587
+ // Label
588
+ var colorHex = '#' + neonColor.toString(16).padStart(6, '0');
589
+ ctx.fillStyle = colorHex;
590
+ ctx.globalAlpha = 0.4;
591
+ ctx.font = 'bold 48px monospace';
592
+ ctx.textAlign = 'center';
593
+ ctx.fillText(label, canvasW / 2, canvasH / 2 - 15);
594
+ ctx.globalAlpha = 0.25;
595
+ ctx.font = '20px monospace';
596
+ ctx.fillText('Awaiting content...', canvasW / 2, canvasH / 2 + 30);
597
+ ctx.globalAlpha = 1;
598
+
599
+ var tex = new THREE.CanvasTexture(cvs);
600
+ tex.minFilter = THREE.LinearFilter;
601
+ var screenMat = new THREE.MeshStandardMaterial({
602
+ map: tex, emissive: 0xffffff, emissiveMap: tex, emissiveIntensity: 0.9, roughness: 0.3, metalness: 0,
603
+ });
604
+ var screen = new THREE.Mesh(new THREE.PlaneGeometry(w, h), screenMat);
605
+ screen.position.z = 0.035;
606
+ group.add(screen);
607
+
608
+ // Neon glow strips (top + bottom of screen)
609
+ var glowBottom = new THREE.Mesh(new THREE.BoxGeometry(w + 0.1, 0.025, 0.02), neonMat);
610
+ glowBottom.position.set(0, -(h / 2) - 0.12, 0.03);
611
+ group.add(glowBottom);
612
+ var glowTop = new THREE.Mesh(new THREE.BoxGeometry(w + 0.1, 0.025, 0.02), neonMat);
613
+ glowTop.position.set(0, (h / 2) + 0.12, 0.03);
614
+ group.add(glowTop);
615
+
616
+ // Label above screen (CSS2D)
617
+ var labelDiv = document.createElement('div');
618
+ labelDiv.textContent = label;
619
+ labelDiv.style.cssText = 'color:' + colorHex + ';font-size:9px;font-weight:bold;font-family:monospace;letter-spacing:3px;text-shadow:0 0 10px ' + colorHex + ',0 0 20px ' + colorHex + '44;';
620
+ var labelObj = new CSS2DObject(labelDiv);
621
+ labelObj.position.set(0, h / 2 + 0.35, 0);
622
+ group.add(labelObj);
623
+
624
+ return { group: group, canvas: cvs, context: ctx, texture: tex, screenMat: screenMat, screenMesh: screen, label: label, canvasW: canvasW, canvasH: canvasH };
625
+ }
626
+
627
+ // ============================================================
628
+ // SCREEN UPDATE LOGIC
629
+ // 3 screens, each shows ONLY its own media type:
630
+ // IMAGE GALLERY (west) — type:'image' — concept art, photos, illustrations
631
+ // VIDEO GALLERY (south) — type:'video' — videos, animations, mp4
632
+ // TEXTURE GALLERY (north) — type:'texture' — seamless textures, materials, PBR
633
+ // ============================================================
634
+
635
+ var _loadedImages = {};
636
+ var _screenStates = {
637
+ image: { index: 0, timer: 0, interval: 6, items: [], page: 0 },
638
+ video: { index: 0, timer: 3, interval: 8, items: [], page: 0 },
639
+ texture: { index: 0, timer: 0, interval: 10, items: [], page: 0 },
640
+ };
641
+
642
+ function _mediaUrl(mediaId) {
643
+ var path = '/api/media/' + mediaId + '/file';
644
+ if (typeof window.scopedApiUrl === 'function') {
645
+ return window.scopedApiUrl(path, null, { includeBranch: false });
646
+ }
647
+ return path + (window.currentProjectPath ? '?project=' + encodeURIComponent(window.currentProjectPath) : '');
648
+ }
649
+
650
+ export function updateGalleryScreens(mediaItems) {
651
+ if (!S.galleryScreens || !mediaItems || mediaItems.length === 0) return;
652
+ S.cachedMedia = mediaItems;
653
+
654
+ // Split by type — each screen gets ONLY its own category
655
+ var images = mediaItems.filter(function(m) { return m.type === 'image'; });
656
+ var videos = mediaItems.filter(function(m) { return m.type === 'video'; });
657
+ var textures = mediaItems.filter(function(m) { return m.type === 'texture'; });
658
+
659
+ // IMAGE GALLERY — single image slideshow
660
+ _screenStates.image.items = images;
661
+ if (S.galleryScreens.image) {
662
+ if (images.length > 0) {
663
+ _showSingleImage(S.galleryScreens.image, images[0], _screenStates.image, 0x06b6d4);
664
+ } else {
665
+ _drawEmptyScreen(S.galleryScreens.image, 'IMAGE GALLERY', 0x06b6d4, 'Generate images to display here');
666
+ }
667
+ }
668
+
669
+ // VIDEO GALLERY — single video/still slideshow (shows thumbnail for videos)
670
+ _screenStates.video.items = videos;
671
+ if (S.galleryScreens.video) {
672
+ if (videos.length > 0) {
673
+ _showSingleImage(S.galleryScreens.video, videos[0], _screenStates.video, 0xec4899);
674
+ } else {
675
+ _drawEmptyScreen(S.galleryScreens.video, 'VIDEO GALLERY', 0xec4899, 'Use "video" or "animation" in prompt');
676
+ }
677
+ }
678
+
679
+ // TEXTURE GALLERY — 3x2 grid view
680
+ _screenStates.texture.items = textures;
681
+ if (S.galleryScreens.texture) {
682
+ if (textures.length > 0) {
683
+ _drawTextureGrid(S.galleryScreens.texture, textures, 0);
684
+ } else {
685
+ _drawEmptyScreen(S.galleryScreens.texture, 'TEXTURE GALLERY', 0x22c55e, 'Use "texture" or "seamless" in prompt');
686
+ }
687
+ }
688
+
689
+ // Update robot desk monitors (mirror wall screens)
690
+ if (S.galleryDeskScreens) {
691
+ if (images.length > 0 && S.galleryDeskScreens.image) {
692
+ _showSingleImage(S.galleryDeskScreens.image, images[0], _screenStates.image, 0x06b6d4);
693
+ }
694
+ if (videos.length > 0 && S.galleryDeskScreens.video) {
695
+ _showSingleImage(S.galleryDeskScreens.video, videos[0], _screenStates.video, 0xec4899);
696
+ }
697
+ if (textures.length > 0 && S.galleryDeskScreens.texture) {
698
+ _showSingleImage(S.galleryDeskScreens.texture, textures[0], _screenStates.texture, 0x22c55e);
699
+ }
700
+ }
701
+ }
702
+
703
+ export function tickGallery(dt) {
704
+ // No auto-advance — user navigates with E/Q while looking at a screen
705
+ }
706
+
707
+ // Navigate a specific screen forward/backward (called from player E/Q raycast)
708
+ export function galleryNavigate(screenName, direction) {
709
+ if (!S.galleryScreens || !S.galleryScreens[screenName]) return;
710
+ var st = _screenStates[screenName];
711
+ if (!st || st.items.length === 0) return;
712
+
713
+ var colors = { image: 0x06b6d4, video: 0xec4899, texture: 0x22c55e };
714
+
715
+ if (screenName === 'texture') {
716
+ var maxPage = Math.ceil(st.items.length / 6) - 1;
717
+ st.page = direction > 0 ? Math.min(st.page + 1, maxPage) : Math.max(st.page - 1, 0);
718
+ _drawTextureGrid(S.galleryScreens.texture, st.items, st.page);
719
+ } else {
720
+ st.index = st.index + direction;
721
+ if (st.index < 0) st.index = st.items.length - 1;
722
+ if (st.index >= st.items.length) st.index = 0;
723
+ _showSingleImage(S.galleryScreens[screenName], st.items[st.index], st, colors[screenName]);
724
+ }
725
+
726
+ // Sync desk monitor
727
+ if (S.galleryDeskScreens && S.galleryDeskScreens[screenName] && st.items.length > 0) {
728
+ var idx = screenName === 'texture' ? Math.min(st.page * 6, st.items.length - 1) : st.index;
729
+ _showSingleImage(S.galleryDeskScreens[screenName], st.items[idx], st, colors[screenName]);
730
+ }
731
+ }
732
+
733
+ // ==================== SINGLE IMAGE RENDERER ====================
734
+
735
+ // ==================== EMPTY SCREEN PLACEHOLDER ====================
736
+
737
+ function _drawEmptyScreen(screen, title, accentColor, hint) {
738
+ if (!screen) return;
739
+ var ctx = screen.context;
740
+ var cw = screen.canvasW, ch = screen.canvasH;
741
+ var hex = '#' + accentColor.toString(16).padStart(6, '0');
742
+
743
+ ctx.fillStyle = '#08080f';
744
+ ctx.fillRect(0, 0, cw, ch);
745
+
746
+ // Subtle grid
747
+ ctx.strokeStyle = 'rgba(40,40,60,0.2)';
748
+ ctx.lineWidth = 0.5;
749
+ for (var gx = 0; gx < cw; gx += 40) {
750
+ ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, ch); ctx.stroke();
751
+ }
752
+ for (var gy = 0; gy < ch; gy += 40) {
753
+ ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(cw, gy); ctx.stroke();
754
+ }
755
+
756
+ // Title
757
+ ctx.fillStyle = hex;
758
+ ctx.globalAlpha = 0.5;
759
+ ctx.font = 'bold 42px monospace';
760
+ ctx.textAlign = 'center';
761
+ ctx.fillText(title, cw / 2, ch / 2 - 20);
762
+
763
+ // Hint
764
+ ctx.globalAlpha = 0.3;
765
+ ctx.font = '18px monospace';
766
+ ctx.fillText(hint || 'Awaiting content...', cw / 2, ch / 2 + 25);
767
+ ctx.globalAlpha = 1;
768
+
769
+ screen.texture.needsUpdate = true;
770
+ }
771
+
772
+ // ==================== SINGLE IMAGE RENDERER ====================
773
+
774
+ function _showSingleImage(screen, mediaItem, state, accentColor) {
775
+ if (!screen || !mediaItem) return;
776
+ var imgUrl = _mediaUrl(mediaItem.id);
777
+
778
+ if (_loadedImages[mediaItem.id]) {
779
+ _drawFittedImage(screen, _loadedImages[mediaItem.id], mediaItem, state, accentColor);
780
+ return;
781
+ }
782
+
783
+ // Show loading state
784
+ var ctx = screen.context;
785
+ var cw = screen.canvasW, ch = screen.canvasH;
786
+ ctx.fillStyle = '#08080f';
787
+ ctx.fillRect(0, 0, cw, ch);
788
+ var hex = '#' + accentColor.toString(16).padStart(6, '0');
789
+ ctx.fillStyle = hex + '66';
790
+ ctx.font = '24px monospace';
791
+ ctx.textAlign = 'center';
792
+ ctx.fillText('Loading...', cw / 2, ch / 2);
793
+ screen.texture.needsUpdate = true;
794
+
795
+ var img = new Image();
796
+ img.crossOrigin = 'anonymous';
797
+ img.onload = function() {
798
+ _loadedImages[mediaItem.id] = img;
799
+ _drawFittedImage(screen, img, mediaItem, state, accentColor);
800
+ };
801
+ img.onerror = function() {
802
+ ctx.fillStyle = '#0a0a10';
803
+ ctx.fillRect(0, 0, cw, ch);
804
+ ctx.fillStyle = '#ef4444';
805
+ ctx.font = '22px monospace';
806
+ ctx.textAlign = 'center';
807
+ ctx.fillText('Failed to load', cw / 2, ch / 2);
808
+ screen.texture.needsUpdate = true;
809
+ };
810
+ img.src = imgUrl;
811
+ }
812
+
813
+ function _drawFittedImage(screen, img, mediaItem, state, accentColor) {
814
+ var ctx = screen.context;
815
+ var cw = screen.canvasW, ch = screen.canvasH;
816
+ var hex = '#' + accentColor.toString(16).padStart(6, '0');
817
+
818
+ // Dark background
819
+ ctx.fillStyle = '#08080f';
820
+ ctx.fillRect(0, 0, cw, ch);
821
+
822
+ // Fit image preserving aspect ratio, with bottom bar for prompt
823
+ var barH = 60;
824
+ var padX = 16, padY = 10;
825
+ var availW = cw - padX * 2;
826
+ var availH = ch - barH - padY * 2;
827
+ var scale = Math.min(availW / img.width, availH / img.height);
828
+ var dw = img.width * scale;
829
+ var dh = img.height * scale;
830
+ var dx = (cw - dw) / 2;
831
+ var dy = padY + (availH - dh) / 2;
832
+
833
+ // Subtle shadow behind image
834
+ ctx.fillStyle = 'rgba(0,0,0,0.4)';
835
+ ctx.fillRect(dx + 3, dy + 3, dw, dh);
836
+
837
+ // Draw image
838
+ ctx.drawImage(img, dx, dy, dw, dh);
839
+
840
+ // Thin accent border around image
841
+ ctx.strokeStyle = hex + '44';
842
+ ctx.lineWidth = 1;
843
+ ctx.strokeRect(dx - 1, dy - 1, dw + 2, dh + 2);
844
+
845
+ // Bottom info bar
846
+ ctx.fillStyle = 'rgba(0,0,0,0.80)';
847
+ ctx.fillRect(0, ch - barH, cw, barH);
848
+ // Accent line at top of bar
849
+ ctx.fillStyle = hex + '88';
850
+ ctx.fillRect(0, ch - barH, cw, 2);
851
+
852
+ // Prompt text (truncated, wrapped to 2 lines)
853
+ var prompt = mediaItem.prompt || '';
854
+ ctx.fillStyle = '#ccc';
855
+ ctx.font = '14px monospace';
856
+ ctx.textAlign = 'left';
857
+ var maxChars = Math.floor((cw - 140) / 8.4);
858
+ var line1 = prompt.substring(0, maxChars);
859
+ var line2 = prompt.length > maxChars ? prompt.substring(maxChars, maxChars * 2) : '';
860
+ if (prompt.length > maxChars * 2) line2 = line2.substring(0, line2.length - 3) + '...';
861
+ ctx.fillText(line1, 12, ch - barH + 22);
862
+ if (line2) ctx.fillText(line2, 12, ch - barH + 40);
863
+
864
+ // Counter + model badge (right side)
865
+ var total = state.items.length;
866
+ var current = state.index + 1;
867
+ ctx.fillStyle = hex;
868
+ ctx.font = 'bold 13px monospace';
869
+ ctx.textAlign = 'right';
870
+ ctx.fillText(current + ' / ' + total, cw - 12, ch - barH + 22);
871
+
872
+ // Model name
873
+ ctx.fillStyle = '#666';
874
+ ctx.font = '11px monospace';
875
+ ctx.fillText(mediaItem.model || mediaItem.provider || '', cw - 12, ch - barH + 40);
876
+
877
+ // Agent who requested
878
+ if (mediaItem.requestedBy) {
879
+ ctx.fillText('by ' + mediaItem.requestedBy, cw - 12, ch - barH + 54);
880
+ }
881
+
882
+ screen.texture.needsUpdate = true;
883
+ }
884
+
885
+ // ==================== TEXTURE GRID RENDERER ====================
886
+
887
+ function _drawTextureGrid(screen, items, page) {
888
+ if (!screen) return;
889
+ var ctx = screen.context;
890
+ var cw = screen.canvasW, ch = screen.canvasH;
891
+
892
+ ctx.fillStyle = '#08080f';
893
+ ctx.fillRect(0, 0, cw, ch);
894
+
895
+ var cols = 3, rows = 2;
896
+ var perPage = cols * rows;
897
+ var startIdx = page * perPage;
898
+ var pageItems = items.slice(startIdx, startIdx + perPage);
899
+ var totalPages = Math.ceil(items.length / perPage);
900
+
901
+ if (pageItems.length === 0) {
902
+ ctx.fillStyle = '#22c55e44';
903
+ ctx.font = 'bold 28px monospace';
904
+ ctx.textAlign = 'center';
905
+ ctx.fillText('TEXTURE GALLERY', cw / 2, ch / 2 - 10);
906
+ ctx.font = '16px monospace';
907
+ ctx.fillText('Awaiting content...', cw / 2, ch / 2 + 20);
908
+ screen.texture.needsUpdate = true;
909
+ return;
910
+ }
911
+
912
+ var pad = 6;
913
+ var headerH = 30;
914
+ var cellW = (cw - pad) / cols;
915
+ var cellH = (ch - headerH - pad) / rows;
916
+
917
+ // Header bar
918
+ ctx.fillStyle = 'rgba(34,197,94,0.12)';
919
+ ctx.fillRect(0, 0, cw, headerH);
920
+ ctx.fillStyle = '#22c55e';
921
+ ctx.font = 'bold 12px monospace';
922
+ ctx.textAlign = 'left';
923
+ ctx.fillText('TEXTURE GALLERY', 10, 19);
924
+ ctx.textAlign = 'right';
925
+ ctx.fillStyle = '#22c55e88';
926
+ ctx.font = '11px monospace';
927
+ ctx.fillText('Page ' + (page + 1) + '/' + totalPages + ' (' + items.length + ' items)', cw - 10, 19);
928
+
929
+ // Grid cells
930
+ pageItems.forEach(function(item, i) {
931
+ var col = i % cols;
932
+ var row = Math.floor(i / cols);
933
+ var x = pad / 2 + col * cellW;
934
+ var y = headerH + pad / 2 + row * cellH;
935
+ var innerW = cellW - pad;
936
+ var innerH = cellH - pad;
937
+
938
+ // Cell background
939
+ ctx.fillStyle = '#0c0c14';
940
+ ctx.fillRect(x, y, innerW, innerH);
941
+
942
+ // Cell border
943
+ ctx.strokeStyle = '#22c55e22';
944
+ ctx.lineWidth = 1;
945
+ ctx.strokeRect(x, y, innerW, innerH);
946
+
947
+ var labelH = 28;
948
+
949
+ if (_loadedImages[item.id]) {
950
+ var loadedImg = _loadedImages[item.id];
951
+ var imgAvailH = innerH - labelH;
952
+ var sc = Math.min((innerW - 8) / loadedImg.width, (imgAvailH - 8) / loadedImg.height);
953
+ var dw = loadedImg.width * sc;
954
+ var dh = loadedImg.height * sc;
955
+ var dx = x + (innerW - dw) / 2;
956
+ var dy = y + 4 + (imgAvailH - dh) / 2;
957
+ ctx.drawImage(loadedImg, dx, dy, dw, dh);
958
+
959
+ // Label
960
+ ctx.fillStyle = 'rgba(0,0,0,0.7)';
961
+ ctx.fillRect(x, y + innerH - labelH, innerW, labelH);
962
+ ctx.fillStyle = '#999';
963
+ ctx.font = '10px monospace';
964
+ ctx.textAlign = 'center';
965
+ var shortPrompt = (item.prompt || '').substring(0, 30);
966
+ if ((item.prompt || '').length > 30) shortPrompt += '...';
967
+ ctx.fillText(shortPrompt, x + innerW / 2, y + innerH - 10);
968
+ } else {
969
+ // Loading placeholder
970
+ ctx.fillStyle = '#22c55e33';
971
+ ctx.font = '12px monospace';
972
+ ctx.textAlign = 'center';
973
+ ctx.fillText('Loading...', x + innerW / 2, y + innerH / 2);
974
+
975
+ // Start loading
976
+ var tImg = new Image();
977
+ tImg.crossOrigin = 'anonymous';
978
+ (function(id, tImg2, screenRef, allItems, pg) {
979
+ tImg2.onload = function() {
980
+ _loadedImages[id] = tImg2;
981
+ _drawTextureGrid(screenRef, allItems, pg);
982
+ };
983
+ })(item.id, tImg, screen, items, page);
984
+ tImg.src = _mediaUrl(item.id);
985
+ }
986
+ });
987
+
988
+ screen.texture.needsUpdate = true;
989
+ }
990
+
991
+ export function getGalleryPosition() {
992
+ return { x: GALLERY_X, z: GALLERY_Z };
993
+ }
994
+
995
+ export function getGalleryDeskPosition() {
996
+ return GALLERY_DESK_POS;
997
+ }