/** * ScenarioProps — Scenario-specific room furniture and props * * Extracted from main.js. Builds and manages visibility of all physical * objects that appear/disappear based on the active scenario: bed, chair, * exercise mat, door, rubble wall, screen/TV, desks, security cameras, * and the alert light system. */ import * as THREE from 'three'; // Scenario-to-prop-name mapping const SCENARIO_PROPS = { empty_room: [], single_breathing: [], two_walking: [], fall_event: [], sleep_monitoring: ['bed'], intrusion_detect: ['door'], gesture_control: ['screen'], crowd_occupancy: ['desk', 'desk2'], search_rescue: ['rubbleWall'], elderly_care: ['chair'], fitness_tracking: ['exerciseMat'], security_patrol: ['camera1', 'camera2'], }; export class ScenarioProps { constructor(scene) { this._scene = scene; this._props = {}; this._currentScenario = null; this._alertLight = null; this._alertIntensity = 0; // Animatable references this._screenGlow = null; this._camera1Group = null; this._camera2Group = null; this._cam1Cone = null; this._cam2Cone = null; this._cam1Led = null; this._cam2Led = null; this._dustParticles = null; this._doorSpotlight = null; this._alarmHousing = null; this._powerLed = null; this._build(); } // ---- helper: positioned box with shadow ---- _box(x, y, z, w, h, d, mat) { const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat); m.position.set(x, y, z); m.castShadow = true; m.receiveShadow = true; return m; } // ---- helper: positioned cylinder with shadow ---- _cyl(x, y, z, rTop, rBot, h, segs, mat) { const m = new THREE.Mesh(new THREE.CylinderGeometry(rTop, rBot, h, segs), mat); m.position.set(x, y, z); m.castShadow = true; m.receiveShadow = true; return m; } // ======================================== // BUILD ALL PROPS // ======================================== _build() { const darkMat = new THREE.MeshStandardMaterial({ color: 0x6b5840, roughness: 0.6, emissive: 0x1a1408, emissiveIntensity: 0.25 }); const metalMat = new THREE.MeshStandardMaterial({ color: 0x808088, roughness: 0.3, metalness: 0.7, emissive: 0x1a1a20, emissiveIntensity: 0.2 }); const accentMat = new THREE.MeshStandardMaterial({ color: 0x606070, roughness: 0.4, metalness: 0.4, emissive: 0x101018, emissiveIntensity: 0.15 }); this._buildBed(darkMat); this._buildChair(darkMat, accentMat); this._buildExerciseMat(); this._buildDoor(); this._buildRubbleWall(); this._buildScreen(metalMat); this._buildDesks(darkMat, metalMat, accentMat); this._buildCameras(metalMat); this._buildAlertSystem(); } // ---- BED (sleep monitoring) ---- _buildBed(darkMat) { const bedGroup = new THREE.Group(); // Bed frame with legs const frameMat = new THREE.MeshStandardMaterial({ color: 0x7a6448, roughness: 0.55, metalness: 0.25, emissive: 0x181008, emissiveIntensity: 0.25 }); const bedFrame = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.12, 1.2), frameMat); bedFrame.position.set(3.5, 0.32, -3.5); bedFrame.castShadow = true; bedGroup.add(bedFrame); // Frame legs (4 short posts) for (const [lx, lz] of [[2.5, -4.0], [4.5, -4.0], [2.5, -3.0], [4.5, -3.0]]) { bedGroup.add(this._cyl(lx, 0.13, lz, 0.04, 0.04, 0.26, 6, frameMat)); } // Headboard — tall panel at head of bed const headboardMat = new THREE.MeshStandardMaterial({ color: 0x6a5440, roughness: 0.65, emissive: 0x140e08, emissiveIntensity: 0.2 }); const headboard = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.7, 1.2), headboardMat); headboard.position.set(2.38, 0.65, -3.5); headboard.castShadow = true; bedGroup.add(headboard); // Mattress const mattressMat = new THREE.MeshStandardMaterial({ color: 0x484860, roughness: 0.75, emissive: 0x0c0c1a, emissiveIntensity: 0.2 }); const mattress = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.15, 1.1), mattressMat); mattress.position.set(3.5, 0.455, -3.5); mattress.castShadow = true; bedGroup.add(mattress); // Wrinkled sheet — wave-displaced plane const sheetGeo = new THREE.PlaneGeometry(1.4, 1.0, 20, 20); const posAttr = sheetGeo.getAttribute('position'); for (let i = 0; i < posAttr.count; i++) { const px = posAttr.getX(i); const py = posAttr.getY(i); posAttr.setZ(i, Math.sin(px * 4) * 0.015 + Math.cos(py * 5) * 0.01 + Math.sin(px * py * 3) * 0.008); } posAttr.needsUpdate = true; sheetGeo.computeVertexNormals(); const sheetMat = new THREE.MeshStandardMaterial({ color: 0x506880, roughness: 0.75, side: THREE.DoubleSide, emissive: 0x0c1018, emissiveIntensity: 0.2, }); const sheet = new THREE.Mesh(sheetGeo, sheetMat); sheet.rotation.x = -Math.PI / 2; sheet.position.set(3.7, 0.54, -3.5); sheet.castShadow = true; bedGroup.add(sheet); // Pillow — soft shape using scaled sphere const pillowGeo = new THREE.SphereGeometry(0.18, 12, 8); pillowGeo.scale(1, 0.35, 1.4); const pillowMat = new THREE.MeshStandardMaterial({ color: 0x706868, roughness: 0.7, emissive: 0x141010, emissiveIntensity: 0.2 }); const pillow = new THREE.Mesh(pillowGeo, pillowMat); pillow.position.set(2.65, 0.52, -3.5); pillow.castShadow = true; bedGroup.add(pillow); // Bedside lamp — small cylinder + sphere shade on a tiny table const lampBaseMat = new THREE.MeshStandardMaterial({ color: 0x686870, roughness: 0.3, metalness: 0.7, emissive: 0x101018, emissiveIntensity: 0.15 }); // Nightstand bedGroup.add(this._box(2.15, 0.25, -3.5, 0.35, 0.5, 0.35, darkMat)); // Lamp base bedGroup.add(this._cyl(2.15, 0.55, -3.5, 0.04, 0.05, 0.1, 8, lampBaseMat)); // Lamp stem bedGroup.add(this._cyl(2.15, 0.68, -3.5, 0.015, 0.015, 0.2, 6, lampBaseMat)); // Lamp shade (emissive warm glow) const shadeMat = new THREE.MeshStandardMaterial({ color: 0x705830, emissive: 0x604018, emissiveIntensity: 1.0, roughness: 0.6, side: THREE.DoubleSide, transparent: true, opacity: 0.9, }); const shade = new THREE.Mesh(new THREE.ConeGeometry(0.08, 0.1, 8, 1, true), shadeMat); shade.position.set(2.15, 0.78, -3.5); shade.rotation.x = Math.PI; bedGroup.add(shade); // Warm lamp light const lampLight = new THREE.PointLight(0xffcc88, 2.0, 6, 1.2); lampLight.position.set(2.15, 0.78, -3.5); bedGroup.add(lampLight); this._props.bed = bedGroup; bedGroup.visible = false; this._scene.add(bedGroup); } // ---- CHAIR (elderly care) ---- _buildChair(darkMat, accentMat) { const chairGroup = new THREE.Group(); chairGroup.position.set(1, 0, -1.5); const cushionMat = new THREE.MeshStandardMaterial({ color: 0x5a5078, roughness: 0.7, emissive: 0x10101a, emissiveIntensity: 0.2 }); // Seat chairGroup.add(this._box(0, 0.45, 0, 0.5, 0.04, 0.45, darkMat)); // Seat cushion — slightly puffy const cushionGeo = new THREE.BoxGeometry(0.46, 0.06, 0.42); // Gentle puff on top vertices const cPos = cushionGeo.getAttribute('position'); for (let i = 0; i < cPos.count; i++) { if (cPos.getY(i) > 0) { const dx = cPos.getX(i) / 0.23; const dz = cPos.getZ(i) / 0.21; cPos.setY(i, cPos.getY(i) + 0.015 * (1 - dx * dx) * (1 - dz * dz)); } } cPos.needsUpdate = true; cushionGeo.computeVertexNormals(); const cushion = new THREE.Mesh(cushionGeo, cushionMat); cushion.position.set(0, 0.50, 0); cushion.castShadow = true; chairGroup.add(cushion); // Back chairGroup.add(this._box(0, 0.72, -0.22, 0.5, 0.5, 0.04, darkMat)); // Legs for (const [lx, lz] of [[-0.22, -0.2], [0.22, -0.2], [-0.22, 0.2], [0.22, 0.2]]) { chairGroup.add(this._box(lx, 0.22, lz, 0.04, 0.44, 0.04, darkMat)); } // Armrests chairGroup.add(this._box(-0.28, 0.6, 0, 0.04, 0.04, 0.4, accentMat)); chairGroup.add(this._box(0.28, 0.6, 0, 0.04, 0.04, 0.4, accentMat)); // Armrest supports chairGroup.add(this._box(-0.28, 0.52, -0.18, 0.04, 0.12, 0.04, accentMat)); chairGroup.add(this._box(0.28, 0.52, -0.18, 0.04, 0.12, 0.04, accentMat)); // Small side table const tableMat = new THREE.MeshStandardMaterial({ color: 0x685840, roughness: 0.55, emissive: 0x14100a, emissiveIntensity: 0.2 }); chairGroup.add(this._box(0.65, 0.3, 0, 0.35, 0.03, 0.35, tableMat)); // Table legs for (const [tx, tz] of [[0.5, -0.14], [0.8, -0.14], [0.5, 0.14], [0.8, 0.14]]) { chairGroup.add(this._cyl(tx, 0.15, tz, 0.015, 0.015, 0.28, 6, tableMat)); } this._props.chair = chairGroup; chairGroup.visible = false; this._scene.add(chairGroup); } // ---- EXERCISE MAT (fitness tracking) ---- _buildExerciseMat() { const matGroup = new THREE.Group(); const matMat = new THREE.MeshStandardMaterial({ color: 0x408858, roughness: 0.75, emissive: 0x0c2010, emissiveIntensity: 0.25 }); // Mat body const exerciseMat = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.015, 0.8), matMat); exerciseMat.position.set(0, 0.008, 0); exerciseMat.receiveShadow = true; matGroup.add(exerciseMat); // Boundary lines on the mat (thin strips) const lineMat = new THREE.MeshStandardMaterial({ color: 0x50a068, roughness: 0.7, emissive: 0x102818, emissiveIntensity: 0.3 }); // Longitudinal borders matGroup.add(this._box(0, 0.017, -0.37, 1.7, 0.003, 0.02, lineMat)); matGroup.add(this._box(0, 0.017, 0.37, 1.7, 0.003, 0.02, lineMat)); // Cross lines (exercise area markers) for (const xOff of [-0.6, 0, 0.6]) { matGroup.add(this._box(xOff, 0.017, 0, 0.02, 0.003, 0.74, lineMat)); } // Water bottle (cylinder body + hemisphere cap) const bottleMat = new THREE.MeshStandardMaterial({ color: 0x4878a8, roughness: 0.2, metalness: 0.7, emissive: 0x0c1828, emissiveIntensity: 0.25 }); const bottleBody = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.18, 10), bottleMat); bottleBody.position.set(1.1, 0.09, 0.25); bottleBody.castShadow = true; matGroup.add(bottleBody); const bottleCap = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2), bottleMat); bottleCap.position.set(1.1, 0.18, 0.25); matGroup.add(bottleCap); // Bottle neck const neckMat = new THREE.MeshStandardMaterial({ color: 0x587088, roughness: 0.3, metalness: 0.6, emissive: 0x0c1420, emissiveIntensity: 0.2 }); matGroup.add(this._cyl(1.1, 0.21, 0.25, 0.018, 0.025, 0.04, 8, neckMat)); // Small towel (flat draped box) const towelMat = new THREE.MeshStandardMaterial({ color: 0x686890, roughness: 0.75, emissive: 0x101020, emissiveIntensity: 0.2 }); const towel = this._box(1.1, 0.01, -0.25, 0.3, 0.008, 0.15, towelMat); towel.rotation.y = 0.15; matGroup.add(towel); this._props.exerciseMat = matGroup; matGroup.visible = false; this._scene.add(matGroup); } // ---- DOOR (intrusion detection) ---- _buildDoor() { const doorGroup = new THREE.Group(); doorGroup.position.set(-5.5, 0, -1); const doorMat = new THREE.MeshStandardMaterial({ color: 0x7a6040, roughness: 0.5, emissive: 0x18140a, emissiveIntensity: 0.25 }); const hingeMat = new THREE.MeshStandardMaterial({ color: 0x909098, roughness: 0.2, metalness: 0.85, emissive: 0x181820, emissiveIntensity: 0.15 }); // Left jamb doorGroup.add(this._box(-0.45, 1.1, 0, 0.08, 2.2, 0.15, doorMat)); // Right jamb doorGroup.add(this._box(0.45, 1.1, 0, 0.08, 2.2, 0.15, doorMat)); // Top doorGroup.add(this._box(0, 2.2, 0, 0.98, 0.08, 0.15, doorMat)); // Door panel (partially open) const doorPanel = new THREE.Mesh(new THREE.BoxGeometry(0.85, 2.1, 0.04), doorMat); doorPanel.position.set(0.2, 1.05, -0.2); doorPanel.rotation.y = -0.7; doorPanel.castShadow = true; doorGroup.add(doorPanel); // Door handle (torus) const handleMat = new THREE.MeshStandardMaterial({ color: 0xaaaaB0, roughness: 0.1, metalness: 0.9, emissive: 0x1a1a20, emissiveIntensity: 0.2 }); const handle = new THREE.Mesh(new THREE.TorusGeometry(0.035, 0.008, 6, 12), handleMat); // Position on the door panel (relative to panel pivot) handle.position.set(0.48, 1.05, -0.22); handle.rotation.y = -0.7; handle.rotation.x = Math.PI / 2; doorGroup.add(handle); // Hinge details — small cylinders at jamb for (const hy of [0.4, 1.1, 1.8]) { const hinge = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.06, 6), hingeMat); hinge.position.set(-0.42, hy, 0.06); doorGroup.add(hinge); } // Light spill through the gap — spotlight from outside const doorSpot = new THREE.SpotLight(0x88aacc, 3.0, 10, Math.PI / 4, 0.3, 0.6); doorSpot.position.set(-0.8, 1.2, -0.5); doorSpot.target.position.set(0.5, 0, 0.5); doorGroup.add(doorSpot); doorGroup.add(doorSpot.target); this._doorSpotlight = doorSpot; // Window next to door — simple frame with translucent pane const windowFrame = new THREE.MeshStandardMaterial({ color: 0x686878, roughness: 0.35, metalness: 0.6, emissive: 0x101018, emissiveIntensity: 0.15 }); // Frame doorGroup.add(this._box(1.2, 1.5, 0, 0.04, 0.8, 0.06, windowFrame)); doorGroup.add(this._box(1.2, 1.5, 0, 0.6, 0.04, 0.06, windowFrame)); doorGroup.add(this._box(0.92, 1.5, 0, 0.04, 0.8, 0.06, windowFrame)); doorGroup.add(this._box(1.48, 1.5, 0, 0.04, 0.8, 0.06, windowFrame)); doorGroup.add(this._box(1.2, 1.1, 0, 0.6, 0.04, 0.06, windowFrame)); doorGroup.add(this._box(1.2, 1.9, 0, 0.6, 0.04, 0.06, windowFrame)); // Glass pane const glassMat = new THREE.MeshStandardMaterial({ color: 0x305880, transparent: true, opacity: 0.4, roughness: 0.05, metalness: 0.3, emissive: 0x0c1830, emissiveIntensity: 0.35, }); const glass = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.72, 0.01), glassMat); glass.position.set(1.2, 1.5, 0); doorGroup.add(glass); this._props.door = doorGroup; doorGroup.visible = false; this._scene.add(doorGroup); } // ---- RUBBLE WALL (search & rescue) ---- _buildRubbleWall() { const rubbleGroup = new THREE.Group(); const rubbleMat = new THREE.MeshStandardMaterial({ color: 0x807868, roughness: 0.75, emissive: 0x181610, emissiveIntensity: 0.25 }); const rebarMat = new THREE.MeshStandardMaterial({ color: 0x8a7858, roughness: 0.4, metalness: 0.7, emissive: 0x1a1408, emissiveIntensity: 0.2 }); // Broken wall — main slab rubbleGroup.add(this._box(2, 1, 0, 0.4, 2, 3, rubbleMat)); // Wall crack lines (thin dark boxes embedded in wall surface) const crackMat = new THREE.MeshStandardMaterial({ color: 0x403828, roughness: 0.9 }); const cracks = [ [1.82, 1.4, -0.3, 0.01, 0.6, 0.02, 0.3], [1.82, 0.8, 0.5, 0.01, 0.5, 0.02, -0.2], [1.82, 1.6, 0.8, 0.01, 0.4, 0.02, 0.15], [1.82, 0.5, -0.7, 0.01, 0.35, 0.02, -0.25], ]; for (const [cx, cy, cz, cw, ch, cd, rot] of cracks) { const crack = this._box(cx, cy, cz, cw, ch, cd, crackMat); crack.rotation.z = rot; rubbleGroup.add(crack); } // Rebar — thin metal cylinders protruding from the wall for (const [rx, ry, rz, rLen, rRot] of [ [1.6, 1.7, -0.4, 0.8, 0.3], [1.5, 1.2, 0.6, 0.6, -0.2], [1.7, 0.9, -0.8, 0.5, 0.5], [1.55, 1.5, 1.0, 0.7, -0.4], ]) { const rebar = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, rLen, 6), rebarMat); rebar.position.set(rx, ry, rz); rebar.rotation.z = Math.PI / 2 + rRot; rebar.rotation.y = rRot * 0.5; rebar.castShadow = true; rubbleGroup.add(rebar); } // Rubble pieces — more varied with random rotations const rubbleColors = [0x807868, 0x706860, 0x908878, 0x686058]; for (let i = 0; i < 10; i++) { const s = 0.12 + Math.random() * 0.3; const rMat = new THREE.MeshStandardMaterial({ color: rubbleColors[i % rubbleColors.length], roughness: 0.7 + Math.random() * 0.15, emissive: 0x141210, emissiveIntensity: 0.2, }); const piece = this._box( 1.3 + Math.random() * 1.4, s / 2, -1.5 + Math.random() * 3, s, s * (0.4 + Math.random() * 0.5), s * (0.6 + Math.random() * 0.4), rMat ); piece.rotation.x = (Math.random() - 0.5) * 0.6; piece.rotation.y = (Math.random() - 0.5) * 1.2; piece.rotation.z = (Math.random() - 0.5) * 0.4; rubbleGroup.add(piece); } // Dust particles near rubble const dustCount = 60; const dustGeo = new THREE.BufferGeometry(); const dustPositions = new Float32Array(dustCount * 3); for (let i = 0; i < dustCount; i++) { dustPositions[i * 3] = 1.0 + Math.random() * 2.0; dustPositions[i * 3 + 1] = Math.random() * 2.5; dustPositions[i * 3 + 2] = -1.5 + Math.random() * 3.0; } dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPositions, 3)); const dustMaterial = new THREE.PointsMaterial({ color: 0xaa9988, size: 0.03, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false, }); this._dustParticles = new THREE.Points(dustGeo, dustMaterial); rubbleGroup.add(this._dustParticles); this._props.rubbleWall = rubbleGroup; rubbleGroup.visible = false; this._scene.add(rubbleGroup); } // ---- SCREEN / TV (gesture control) ---- _buildScreen(metalMat) { const screenGroup = new THREE.Group(); const screenFrame = new THREE.MeshStandardMaterial({ color: 0x484850, roughness: 0.2, metalness: 0.7, emissive: 0x0c0c14, emissiveIntensity: 0.15 }); // Frame screenGroup.add(this._box(0, 1.5, -4.7, 1.8, 1.1, 0.06, screenFrame)); // Screen surface (emissive, color shifts in update()) const screenSurfMat = new THREE.MeshStandardMaterial({ color: 0x1a3868, emissive: 0x1a3868, emissiveIntensity: 1.2, roughness: 0.1, }); const screenSurf = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.9, 0.02), screenSurfMat); screenSurf.position.set(0, 1.5, -4.66); screenGroup.add(screenSurf); this._screenGlow = screenSurfMat; // Stand / mount — neck + base screenGroup.add(this._box(0, 0.88, -4.7, 0.08, 0.16, 0.08, screenFrame)); screenGroup.add(this._box(0, 0.78, -4.7, 0.4, 0.03, 0.2, metalMat)); // Power LED indicator const ledMat = new THREE.MeshStandardMaterial({ color: 0x00ff40, emissive: 0x00ff40, emissiveIntensity: 1.0, }); const powerLed = new THREE.Mesh(new THREE.SphereGeometry(0.012, 6, 4), ledMat); powerLed.position.set(0.82, 0.96, -4.66); screenGroup.add(powerLed); this._powerLed = ledMat; // Subtle screen glow (point light) const screenLight = new THREE.PointLight(0x4080e0, 1.5, 6); screenLight.position.set(0, 1.5, -4.5); screenGroup.add(screenLight); // Media console below the screen const consoleMat = new THREE.MeshStandardMaterial({ color: 0x484858, roughness: 0.45, metalness: 0.5, emissive: 0x0c0c14, emissiveIntensity: 0.15 }); screenGroup.add(this._box(0, 0.55, -4.7, 1.2, 0.35, 0.35, consoleMat)); // Console shelf divider screenGroup.add(this._box(0, 0.55, -4.54, 1.1, 0.02, 0.01, metalMat)); this._props.screen = screenGroup; screenGroup.visible = false; this._scene.add(screenGroup); } // ---- DESKS (crowd / office) ---- _buildDesks(darkMat, metalMat, accentMat) { // Desk 1 (left) const deskGroup = new THREE.Group(); deskGroup.add(this._box(-2, 0.38, -1, 1.2, 0.04, 0.6, darkMat)); for (const [lx, lz] of [[-2.55, -1.25], [-1.45, -1.25], [-2.55, -0.75], [-1.45, -0.75]]) { deskGroup.add(this._box(lx, 0.19, lz, 0.04, 0.38, 0.04, darkMat)); } // Monitor on desk 1 const monitorMat = new THREE.MeshStandardMaterial({ color: 0x484850, roughness: 0.2, metalness: 0.7, emissive: 0x0c0c14, emissiveIntensity: 0.15 }); const monScreenMat = new THREE.MeshStandardMaterial({ color: 0x183858, emissive: 0x183858, emissiveIntensity: 1.0, roughness: 0.1, }); deskGroup.add(this._box(-2, 0.62, -1.15, 0.5, 0.35, 0.03, monitorMat)); deskGroup.add(this._box(-2, 0.62, -1.13, 0.44, 0.29, 0.01, monScreenMat)); deskGroup.add(this._box(-2, 0.42, -1.1, 0.06, 0.04, 0.06, metalMat)); // stand neck deskGroup.add(this._box(-2, 0.40, -1.05, 0.18, 0.01, 0.12, metalMat)); // stand base // Keyboard outline deskGroup.add(this._box(-2, 0.405, -0.85, 0.35, 0.008, 0.12, accentMat)); // Office chair at desk 1 this._buildOfficeChair(deskGroup, -2, -0.55, darkMat, metalMat); // Monitor glow light const monLight = new THREE.PointLight(0x4080e0, 1.2, 4); monLight.position.set(-2, 0.7, -1.0); deskGroup.add(monLight); this._props.desk = deskGroup; deskGroup.visible = false; this._scene.add(deskGroup); // Desk 2 (right) const desk2Group = new THREE.Group(); desk2Group.add(this._box(2, 0.38, 1, 1.0, 0.04, 0.6, darkMat)); for (const [lx, lz] of [[1.45, 0.75], [2.55, 0.75], [1.45, 1.25], [2.55, 1.25]]) { desk2Group.add(this._box(lx, 0.19, lz, 0.04, 0.38, 0.04, darkMat)); } // Monitor on desk 2 desk2Group.add(this._box(2, 0.62, 1.15, 0.5, 0.35, 0.03, monitorMat)); desk2Group.add(this._box(2, 0.62, 1.17, 0.44, 0.29, 0.01, monScreenMat)); desk2Group.add(this._box(2, 0.42, 1.1, 0.06, 0.04, 0.06, metalMat)); desk2Group.add(this._box(2, 0.40, 1.05, 0.18, 0.01, 0.12, metalMat)); // Keyboard desk2Group.add(this._box(2, 0.405, 0.85, 0.35, 0.008, 0.12, accentMat)); // Office chair at desk 2 this._buildOfficeChair(desk2Group, 2, 0.55, darkMat, metalMat); // Water cooler / plant between desks area const plantMat = new THREE.MeshStandardMaterial({ color: 0x2a7838, roughness: 0.7, emissive: 0x0c2810, emissiveIntensity: 0.3 }); const potMat = new THREE.MeshStandardMaterial({ color: 0x706858, roughness: 0.6, emissive: 0x14120c, emissiveIntensity: 0.15 }); desk2Group.add(this._cyl(3.2, 0.15, 0, 0.12, 0.1, 0.3, 8, potMat)); // Foliage — cluster of small spheres for (const [fx, fy, fz] of [[3.2, 0.45, 0], [3.15, 0.4, 0.06], [3.25, 0.42, -0.05]]) { const leaf = new THREE.Mesh(new THREE.SphereGeometry(0.08, 6, 5), plantMat); leaf.position.set(fx, fy, fz); desk2Group.add(leaf); } // Monitor glow light const monLight2 = new THREE.PointLight(0x4080e0, 1.2, 4); monLight2.position.set(2, 0.7, 1.0); desk2Group.add(monLight2); this._props.desk2 = desk2Group; desk2Group.visible = false; this._scene.add(desk2Group); } // Helper: small office chair _buildOfficeChair(parent, x, z, darkMat, metalMat) { // Seat parent.add(this._box(x, 0.38, z, 0.35, 0.03, 0.35, darkMat)); // Backrest parent.add(this._box(x, 0.55, z - 0.16, 0.32, 0.3, 0.03, darkMat)); // Central post parent.add(this._cyl(x, 0.22, z, 0.025, 0.025, 0.28, 6, metalMat)); // Base star (5 legs) for (let i = 0; i < 5; i++) { const angle = (i / 5) * Math.PI * 2; const legLen = 0.16; const leg = this._box( x + Math.cos(angle) * legLen * 0.5, 0.04, z + Math.sin(angle) * legLen * 0.5, legLen, 0.015, 0.025, metalMat ); leg.rotation.y = -angle; parent.add(leg); } } // ---- SECURITY CAMERAS (patrol) ---- _buildCameras(metalMat) { const camData = [ ['camera1', [5, 3.5, -4.5]], ['camera2', [-5, 3.5, 4.5]], ]; for (const [name, pos] of camData) { const camGroup = new THREE.Group(); camGroup.position.set(...pos); // Camera body camGroup.add(this._box(0, 0, 0, 0.15, 0.1, 0.2, metalMat)); // Lens const lens = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.08, 8), metalMat); lens.rotation.x = Math.PI / 2; lens.position.z = 0.14; camGroup.add(lens); // Bracket / mount arm camGroup.add(this._box(0, 0.1, -0.08, 0.04, 0.2, 0.04, metalMat)); // Rotating motor housing (visible joint) const motorMat = new THREE.MeshStandardMaterial({ color: 0x686870, roughness: 0.35, metalness: 0.8, emissive: 0x141418, emissiveIntensity: 0.15 }); const motor = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.04, 8), motorMat); motor.position.set(0, 0.05, -0.08); camGroup.add(motor); // FOV cone (semi-transparent) const coneMat = new THREE.MeshStandardMaterial({ color: 0xff3040, transparent: true, opacity: 0.15, side: THREE.DoubleSide, depthWrite: false, emissive: 0xff2020, emissiveIntensity: 0.3, }); const cone = new THREE.Mesh(new THREE.ConeGeometry(1.5, 3, 16, 1, true), coneMat); cone.rotation.x = Math.PI / 2; cone.position.z = 1.7; camGroup.add(cone); // Status LED (blinks in update) const ledMat = new THREE.MeshStandardMaterial({ color: 0xff2020, emissive: 0xff2020, emissiveIntensity: 1.0, }); const led = new THREE.Mesh(new THREE.SphereGeometry(0.015, 6, 4), ledMat); led.position.set(0.08, 0.04, 0.08); camGroup.add(led); this._props[name] = camGroup; camGroup.visible = false; this._scene.add(camGroup); // Store references for animation if (name === 'camera1') { this._camera1Group = camGroup; this._cam1Cone = cone; this._cam1Led = ledMat; } else { this._camera2Group = camGroup; this._cam2Cone = cone; this._cam2Led = ledMat; } } } // ---- ALERT SYSTEM ---- _buildAlertSystem() { // Main alert point light this._alertLight = new THREE.PointLight(0xff3040, 0, 10); this._alertLight.position.set(0, 3.5, 0); this._scene.add(this._alertLight); // Ceiling-mounted alarm housing const housingMat = new THREE.MeshStandardMaterial({ color: 0x686878, roughness: 0.35, metalness: 0.6, emissive: 0x101018, emissiveIntensity: 0.15 }); const housing = new THREE.Group(); // Base plate housing.add(this._box(0, 3.95, 0, 0.2, 0.02, 0.2, housingMat)); // Housing body housing.add(this._cyl(0, 3.85, 0, 0.08, 0.1, 0.16, 8, housingMat)); // Alarm lens (red when active, dark when inactive) const lensMat = new THREE.MeshStandardMaterial({ color: 0x330808, emissive: 0x000000, emissiveIntensity: 0, roughness: 0.2, transparent: true, opacity: 0.8, }); const alarmLens = new THREE.Mesh(new THREE.SphereGeometry(0.06, 10, 8, 0, Math.PI * 2, 0, Math.PI / 2), lensMat); alarmLens.position.set(0, 3.76, 0); alarmLens.rotation.x = Math.PI; housing.add(alarmLens); this._alarmHousing = housing; this._alarmLensMat = lensMat; this._scene.add(housing); } // ======================================== // UPDATE (called every frame) // ======================================== update(data, currentScenario) { const scenario = data?.scenario || currentScenario; const elapsed = Date.now() * 0.001; // Switch visible props when scenario changes if (scenario !== this._currentScenario) { this._currentScenario = scenario; for (const prop of Object.values(this._props)) prop.visible = false; const propsToShow = SCENARIO_PROPS[scenario] || []; for (const name of propsToShow) { if (this._props[name]) this._props[name].visible = true; } } // --- Alert light (fall / intrusion) --- const cls = data?.classification || {}; if (cls.fall_detected || cls.intrusion) { this._alertIntensity = Math.min(2, this._alertIntensity + 0.1); } else { this._alertIntensity = Math.max(0, this._alertIntensity - 0.05); } // Sawtooth pattern for urgency instead of smooth sine const alertPhase = (elapsed * 3) % 1.0; const sawtooth = alertPhase < 0.5 ? alertPhase * 2 : 2 - alertPhase * 2; this._alertLight.intensity = this._alertIntensity * sawtooth; // Alarm housing lens glow tracks alert if (this._alarmLensMat) { const alertFrac = Math.min(this._alertIntensity / 2, 1); this._alarmLensMat.emissive.setHex(alertFrac > 0.05 ? 0xff2020 : 0x000000); this._alarmLensMat.emissiveIntensity = alertFrac * sawtooth; } // Subtle ambient color shift during alerts if (this._alertIntensity > 0.1 && this._alertLight) { const r = 0.08 + 0.04 * sawtooth * this._alertIntensity; const g = 0.05 - 0.02 * this._alertIntensity; const b = 0.10 - 0.04 * this._alertIntensity; // Shift the alert light color slightly over time this._alertLight.color.setRGB( Math.max(0, Math.min(1, 1.0)), Math.max(0, Math.min(1, 0.15 - 0.1 * sawtooth)), Math.max(0, Math.min(1, 0.2 - 0.15 * sawtooth)) ); } else if (this._alertLight) { this._alertLight.color.setHex(0xff3040); } // --- Camera rotation animation --- if (this._camera1Group && this._camera1Group.visible) { this._camera1Group.rotation.y = Math.sin(elapsed * 0.4) * 0.5; } if (this._camera2Group && this._camera2Group.visible) { this._camera2Group.rotation.y = Math.sin(elapsed * 0.4 + Math.PI) * 0.5; } // Camera LED blink if (this._cam1Led && this._camera1Group?.visible) { this._cam1Led.emissiveIntensity = (Math.sin(elapsed * 4) > 0.3) ? 1.0 : 0.1; } if (this._cam2Led && this._camera2Group?.visible) { this._cam2Led.emissiveIntensity = (Math.sin(elapsed * 4 + 1) > 0.3) ? 1.0 : 0.1; } // --- Screen glow color shift --- if (this._screenGlow && this._props.screen?.visible) { const hue = (elapsed * 0.03) % 1; const r = 0.10 + 0.06 * Math.sin(hue * Math.PI * 2); const g = 0.16 + 0.08 * Math.sin(hue * Math.PI * 2 + 2.1); const b = 0.28 + 0.12 * Math.sin(hue * Math.PI * 2 + 4.2); this._screenGlow.emissive.setRGB(r, g, b); } // Power LED gentle pulse if (this._powerLed && this._props.screen?.visible) { this._powerLed.emissiveIntensity = 0.5 + 0.5 * Math.sin(elapsed * 2); } // --- Dust particle drift near rubble --- if (this._dustParticles && this._props.rubbleWall?.visible) { const dPos = this._dustParticles.geometry.getAttribute('position'); for (let i = 0; i < dPos.count; i++) { let y = dPos.getY(i) + 0.002 * Math.sin(elapsed + i); if (y > 2.5) y = 0; dPos.setY(i, y); dPos.setX(i, dPos.getX(i) + Math.sin(elapsed * 0.5 + i * 0.3) * 0.0005); } dPos.needsUpdate = true; } } }