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.
- package/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7216
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- 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
|
+
}
|