|
| 1 | +import * as THREE from "three"; |
| 2 | +import { VRButton } from "three/addons/webxr/VRButton.js"; |
| 3 | +import { CHARACTER_MODES_REGISTRY } from "./modes/modes_registry.js"; |
| 4 | + |
| 5 | +const statusEl = document.getElementById("status"); |
| 6 | +const startButton = document.getElementById("start-btn"); |
| 7 | +const joinPanel = document.getElementById("join-panel"); |
| 8 | +const joinButton = document.getElementById("join-btn"); |
| 9 | +const characterModeSelect = document.getElementById("character-mode"); |
| 10 | +let selectedCharacterMode = "demo-spline"; |
| 11 | + |
| 12 | +const searchParams = new URLSearchParams(window.location.search); |
| 13 | +const defaultWsScheme = window.location.protocol === "https:" ? "wss" : "ws"; |
| 14 | +const defaultWsHost = window.location.hostname || "127.0.0.1"; |
| 15 | +const defaultWsPort = searchParams.get("ws_port") ?? "8765"; |
| 16 | +const serverHost = |
| 17 | + searchParams.get("ws") ?? |
| 18 | + `${defaultWsScheme}://${defaultWsHost}:${defaultWsPort}`; |
| 19 | + |
| 20 | +const calibrationOffset = new THREE.Vector3( |
| 21 | + Number(searchParams.get("ox") ?? 0.0), |
| 22 | + Number(searchParams.get("oy") ?? 0.0), |
| 23 | + Number(searchParams.get("oz") ?? 0.0) |
| 24 | +); |
| 25 | + |
| 26 | +let socket = null; |
| 27 | +let userId = null; |
| 28 | +let sessionRole = "vr_client"; |
| 29 | +let sessionMode = "vr_client"; |
| 30 | + |
| 31 | +const scene = new THREE.Scene(); |
| 32 | +scene.background = new THREE.Color(0x1a1f2b); |
| 33 | +const worldRoot = new THREE.Group(); |
| 34 | +worldRoot.position.copy(calibrationOffset); |
| 35 | +scene.add(worldRoot); |
| 36 | + |
| 37 | +const camera = new THREE.PerspectiveCamera( |
| 38 | + 70, |
| 39 | + window.innerWidth / window.innerHeight, |
| 40 | + 0.01, |
| 41 | + 50 |
| 42 | +); |
| 43 | +camera.position.set(0, 1.4, 2.0); |
| 44 | + |
| 45 | +const renderer = new THREE.WebGLRenderer({ antialias: true }); |
| 46 | +renderer.setSize(window.innerWidth, window.innerHeight); |
| 47 | +renderer.xr.enabled = true; |
| 48 | +document.body.appendChild(renderer.domElement); |
| 49 | + |
| 50 | +const hemi = new THREE.HemisphereLight(0xffffff, 0x223344, 1.2); |
| 51 | +scene.add(hemi); |
| 52 | + |
| 53 | +const grid = new THREE.GridHelper(8, 16, 0x6c757d, 0x495057); |
| 54 | +grid.position.y = 0; |
| 55 | +worldRoot.add(grid); |
| 56 | +const worldAxes = new THREE.AxesHelper(0.3); |
| 57 | +worldRoot.add(worldAxes); |
| 58 | + |
| 59 | +function setStatus(text) { |
| 60 | + statusEl.textContent = text; |
| 61 | +} |
| 62 | + |
| 63 | +function applyCharacterMode(modeKey) { |
| 64 | + // TODO: Implement character mode selection |
| 65 | +} |
| 66 | + |
| 67 | +function connect(joinConfig) { |
| 68 | + socket = new WebSocket(serverHost); |
| 69 | + sessionRole = joinConfig.serverRole ?? joinConfig.role; |
| 70 | + sessionMode = joinConfig.role; |
| 71 | + |
| 72 | + socket.onopen = () => { |
| 73 | + setStatus(`connected: ${serverHost}`); |
| 74 | + socket.send( |
| 75 | + JSON.stringify({ |
| 76 | + version: 1, |
| 77 | + type: "hello", |
| 78 | + payload: { |
| 79 | + client: "sparc-webxr", |
| 80 | + role: sessionRole, |
| 81 | + }, |
| 82 | + }) |
| 83 | + ); |
| 84 | + }; |
| 85 | + |
| 86 | + socket.onclose = (event) => { |
| 87 | + setStatus(`disconnected: ${serverHost} (code=${event.code})`); |
| 88 | + }; |
| 89 | + socket.onerror = () => { |
| 90 | + setStatus(`error: failed to connect ${serverHost}`); |
| 91 | + }; |
| 92 | + |
| 93 | + socket.onmessage = (event) => { |
| 94 | + // TODO: Implement message handling |
| 95 | + |
| 96 | + const message = JSON.parse(event.data); |
| 97 | + |
| 98 | + if (message.type === "error") { |
| 99 | + setStatus(`server error: ${message.payload?.reason ?? "unknown"}`); |
| 100 | + } |
| 101 | + |
| 102 | + }; |
| 103 | +} |
| 104 | + |
| 105 | +function sendXRInput(frame) { |
| 106 | + // TODO: Implement XR input sending |
| 107 | +} |
| 108 | + |
| 109 | +function animate() { |
| 110 | + // TODO: Implement animation loop |
| 111 | + let lastTime = performance.now() / 1000; |
| 112 | + renderer.setAnimationLoop((_, frame) => { |
| 113 | + const now = performance.now() / 1000; |
| 114 | + const dt = Math.min(0.05, now - lastTime); |
| 115 | + lastTime = now; |
| 116 | + |
| 117 | + if (frame) sendXRInput(frame); |
| 118 | + |
| 119 | + renderer.render(scene, camera); |
| 120 | + }); |
| 121 | +} |
| 122 | + |
| 123 | +startButton.addEventListener("click", () => { |
| 124 | + document.body.appendChild(VRButton.createButton(renderer)); |
| 125 | + startButton.remove(); |
| 126 | +}); |
| 127 | + |
| 128 | +window.addEventListener("resize", () => { |
| 129 | + camera.aspect = window.innerWidth / window.innerHeight; |
| 130 | + camera.updateProjectionMatrix(); |
| 131 | + renderer.setSize(window.innerWidth, window.innerHeight); |
| 132 | +}); |
| 133 | + |
| 134 | +/* TODO: Implement key handling for desktop mode */ |
| 135 | +window.addEventListener("keydown", (event) => { |
| 136 | + const key = event.key.toLowerCase(); |
| 137 | + // TODO: Implement key handling |
| 138 | +}); |
| 139 | + |
| 140 | +window.addEventListener("keyup", (event) => { |
| 141 | + const key = event.key.toLowerCase(); |
| 142 | + // TODO: Implement key handling |
| 143 | +}); |
| 144 | +/* TODO */ |
| 145 | + |
| 146 | +renderer.domElement.addEventListener("click", () => { |
| 147 | + if (!renderer.xr.isPresenting && document.pointerLockElement !== renderer.domElement) { |
| 148 | + renderer.domElement.requestPointerLock(); |
| 149 | + } |
| 150 | +}); |
| 151 | + |
| 152 | +document.addEventListener("mousemove", (event) => { |
| 153 | + if (document.pointerLockElement !== renderer.domElement) return; |
| 154 | + if (renderer.xr.isPresenting) return; |
| 155 | + |
| 156 | + // TODO: Implement desktop mode mouse movement handling |
| 157 | +}); |
| 158 | + |
| 159 | +function setupJoinPanel() { |
| 160 | + // TODO: Implement join panel setup |
| 161 | +} |
| 162 | + |
| 163 | +function populateCharacterModeSelect() { |
| 164 | + characterModeSelect.replaceChildren(); |
| 165 | + for (const { id, label } of CHARACTER_MODES_REGISTRY) { |
| 166 | + const opt = document.createElement("option"); |
| 167 | + opt.value = id; |
| 168 | + opt.textContent = label; |
| 169 | + characterModeSelect.appendChild(opt); |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +populateCharacterModeSelect(); |
| 174 | +setupJoinPanel(); |
| 175 | +animate(); |
0 commit comments