awacke1 commited on
Commit
04b8433
·
verified ·
1 Parent(s): 836475d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +408 -328
index.html CHANGED
@@ -1,7 +1,7 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <title>Three.js Shared World (v2)</title>
5
  <style>
6
  body { margin: 0; overflow: hidden; }
7
  canvas { display: block; }
@@ -25,27 +25,38 @@
25
  const keysPressed = {};
26
  const playerSpeed = 0.15;
27
 
28
- // --- State Management ---
29
- let newlyPlacedObjects = []; // Track objects added THIS client, THIS session, NOT YET SAVED
30
- const allLoadedObjects = new Map(); // Map<obj_id, THREE.Object3D> - ALL objects in the scene (initial + new)
31
- const groundMeshes = {}; // Store references to ground meshes: 'x_z' string key -> mesh
32
 
33
- // --- Session Storage Key for unsaved local changes ---
34
- const SESSION_STORAGE_KEY = 'unsavedInfiniteWorldState_v2';
 
 
35
 
36
- // --- Access State from Streamlit (Injected on page load/rerun) ---
37
- const allInitialObjects = window.ALL_INITIAL_OBJECTS || []; // Authoritative state from server on load
38
- const plotsMetadata = window.PLOTS_METADATA || []; // List of saved plot info
39
- let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None"; // User's current tool
40
  const plotWidth = window.PLOT_WIDTH || 50.0;
41
  const plotDepth = window.PLOT_DEPTH || 50.0;
42
 
43
- const groundMaterial = new THREE.MeshStandardMaterial({
44
- color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide, name: "ground_saved"
45
- });
46
- const placeholderGroundMaterial = new THREE.MeshStandardMaterial({
47
- color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide, name: "ground_placeholder"
48
- });
 
 
 
 
 
 
 
 
 
 
49
 
50
 
51
  function init() {
@@ -53,13 +64,14 @@
53
  scene.background = new THREE.Color(0xabcdef);
54
 
55
  const aspect = window.innerWidth / window.innerHeight;
56
- camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000);
57
  camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
58
  camera.lookAt(plotWidth/2, 0, plotDepth/2);
59
  scene.add(camera);
60
 
61
  setupLighting();
62
- setupInitialGround(); // Based on plotsMetadata
 
63
  setupPlayer();
64
 
65
  raycaster = new THREE.Raycaster();
@@ -71,18 +83,8 @@
71
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
72
  document.body.appendChild(renderer.domElement);
73
 
74
- // --- Load Initial Objects from Server Injection ---
75
- console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
76
- allInitialObjects.forEach(objData => {
77
- const mesh = createAndPlaceObject(objData, false); // false = not newly placed by this client session
78
- if(mesh) {
79
- allLoadedObjects.set(mesh.userData.obj_id, mesh); // Track all objects
80
- }
81
- });
82
- console.log("Finished loading initial objects.");
83
-
84
- // --- Restore locally unsaved objects from sessionStorage ---
85
- restoreUnsavedState(); // Add objects that were placed locally but not saved before a refresh
86
 
87
  // Event Listeners
88
  document.addEventListener('mousemove', onMouseMove, false);
@@ -93,211 +95,371 @@
93
 
94
  // --- Define global functions needed by Python ---
95
  window.teleportPlayer = teleportPlayer;
96
- window.getSaveDataAndPosition = getSaveDataAndPosition; // For save button
97
- window.resetNewlyPlacedObjects = resetNewlyPlacedObjects; // Called by Python after successful save
98
- window.updateSelectedObjectType = updateSelectedObjectType; // Called by Python selectbox change
99
 
100
- console.log("Three.js Initialized. World ready.");
101
  animate();
102
  }
103
 
104
- function setupLighting() {
105
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
106
- scene.add(ambientLight);
107
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
108
- directionalLight.position.set(75, 150, 100);
109
- directionalLight.castShadow = true;
110
- directionalLight.shadow.mapSize.width = 2048;
111
- directionalLight.shadow.mapSize.height = 2048;
112
- directionalLight.shadow.camera.near = 10;
113
- directionalLight.shadow.camera.far = 400;
114
- directionalLight.shadow.camera.left = -150;
115
- directionalLight.shadow.camera.right = 150;
116
- directionalLight.shadow.camera.top = 150;
117
- directionalLight.shadow.camera.bottom = -150;
118
- directionalLight.shadow.bias = -0.002;
119
- scene.add(directionalLight);
120
- const hemiLight = new THREE.HemisphereLight( 0xabcdef, 0x55aa55, 0.5 );
121
- scene.add( hemiLight );
122
- }
123
 
124
- function setupInitialGround() {
125
- console.log(`Setting up initial ground for ${plotsMetadata.length} saved plots.`);
126
- plotsMetadata.forEach(plot => {
127
- createGroundPlane(plot.grid_x, plot.grid_z, false);
128
- });
129
- if (!groundMeshes['0_0']) {
130
- createGroundPlane(0, 0, false);
131
- }
132
- // Also create placeholders around initial player position
133
- checkAndExpandGroundVisuals();
134
- }
135
 
136
- function createGroundPlane(gridX, gridZ, isPlaceholder) {
137
- const gridKey = `${gridX}_${gridZ}`;
138
- if (groundMeshes[gridKey]) return groundMeshes[gridKey];
139
-
140
- const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
141
- const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
142
- const groundMesh = new THREE.Mesh(groundGeometry, material);
143
-
144
- groundMesh.rotation.x = -Math.PI / 2;
145
- groundMesh.position.y = -0.05;
146
- groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
147
- groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
148
- groundMesh.receiveShadow = true;
149
- groundMesh.userData.gridKey = gridKey;
150
- groundMesh.userData.isPlaceholder = isPlaceholder;
151
- scene.add(groundMesh);
152
- groundMeshes[gridKey] = groundMesh;
153
- return groundMesh;
154
- }
155
 
156
- function setupPlayer() {
157
- const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
158
- const playerMat = new THREE.MeshStandardMaterial({ color: 0x0055ff, roughness: 0.6 });
159
- playerMesh = new THREE.Mesh(playerGeo, playerMat);
160
- playerMesh.position.set(plotWidth / 2, 0.8, plotDepth / 2);
161
- playerMesh.castShadow = true;
162
- playerMesh.receiveShadow = false;
163
- scene.add(playerMesh);
 
 
 
 
 
 
 
 
 
 
 
164
  }
165
 
166
- // Function to create object mesh based on data
167
- // Returns the created mesh or null
168
- function createAndPlaceObject(objData, isNewUnsavedObject) {
169
- if (!objData || !objData.type || !objData.obj_id) {
170
- console.warn("Skipping object creation - Missing type or obj_id:", objData);
171
- return null;
 
172
  }
173
- // Avoid recreating objects that already exist in the scene (e.g., from initial load + sessionStorage restore)
174
- if (allLoadedObjects.has(objData.obj_id)) {
175
- console.log(`Object ${objData.obj_id} already exists. Skipping creation.`);
176
- // If restoring, ensure it's tracked in newlyPlacedObjects if it should be
177
- const existingMesh = allLoadedObjects.get(objData.obj_id);
178
- if (isNewUnsavedObject && !newlyPlacedObjects.some(o => o === existingMesh)) {
179
- newlyPlacedObjects.push(existingMesh);
180
- existingMesh.userData.isNewlyPlaced = true; // Ensure flag is set
181
- }
182
- return existingMesh;
183
- }
184
 
 
 
185
 
186
- let mesh = null;
187
- const objType = objData.type;
188
-
189
- switch (objType) {
190
- case "Simple House": mesh = createSimpleHouse(); break;
191
- case "Tree": mesh = createTree(); break;
192
- case "Rock": mesh = createRock(); break;
193
- case "Fence Post": mesh = createFencePost(); break;
194
- default: console.warn("Unknown object type:", objType); return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  }
 
196
 
197
- if (mesh) {
198
- mesh.userData.obj_id = objData.obj_id; // Assign the ID from data
199
- mesh.userData.type = objType;
200
- mesh.userData.isNewlyPlaced = isNewUnsavedObject; // Mark if local & unsaved
 
 
 
 
 
 
201
 
202
- if (objData.position) {
203
- mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
204
- }
205
- if (objData.rotation) {
206
- mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
207
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
- scene.add(mesh);
210
- allLoadedObjects.set(objData.obj_id, mesh); // Track it
211
 
212
- if (isNewUnsavedObject) {
213
- newlyPlacedObjects.push(mesh);
214
- // console.log(`Tracked new local object: ${objType} (${mesh.userData.obj_id}). Total unsaved: ${newlyPlacedObjects.length}`);
215
- }
216
- return mesh;
217
  }
218
- return null;
219
- }
220
 
 
 
 
221
 
222
- // --- Object Creation Functions (Factories) ---
223
- // (Keep your createSimpleHouse, createTree, createRock, createFencePost functions here)
224
- // ... (same as previous versions) ...
225
- function createObjectBase(type) { return { userData: { type: type } }; }
226
- function createSimpleHouse() { const base = createObjectBase("Simple House"); const group = new THREE.Group(); Object.assign(group, base); const mat1=new THREE.MeshStandardMaterial({color:0xffccaa,roughness:0.8}), mat2=new THREE.MeshStandardMaterial({color:0xaa5533,roughness:0.7}); const m1=new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5),mat1); m1.position.y=1.5/2; m1.castShadow=true; m1.receiveShadow=true; group.add(m1); const m2=new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4),mat2); m2.position.y=1.5+1/2; m2.rotation.y=Math.PI/4; m2.castShadow=true; m2.receiveShadow=false; group.add(m2); return group; }
227
- function createTree() { const base=createObjectBase("Tree"); const group=new THREE.Group(); Object.assign(group,base); const mat1=new THREE.MeshStandardMaterial({color:0x8B4513,roughness:0.9}), mat2=new THREE.MeshStandardMaterial({color:0x228B22,roughness:0.8}); const m1=new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.4,2,8),mat1); m1.position.y=1; m1.castShadow=true; m1.receiveShadow=true; group.add(m1); const m2=new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0),mat2); m2.position.y=2.8; m2.castShadow=true; m2.receiveShadow=false; group.add(m2); return group; }
228
- function createRock() { const base=createObjectBase("Rock"); const mat=new THREE.MeshStandardMaterial({color:0xaaaaaa,roughness:0.8,metalness:0.1}); const geo = new THREE.IcosahedronGeometry(0.7, 1); geo.positionData = geo.attributes.position.array; for (let i = 0; i < geo.positionData.length; i += 3) { const noise = Math.random() * 0.15 - 0.075; geo.positionData[i] *= (1 + noise); geo.positionData[i+1] *= (1 + noise); geo.positionData[i+2] *= (1 + noise); } geo.computeVertexNormals(); const rock=new THREE.Mesh(geo,mat); Object.assign(rock,base); rock.position.y=0.35; rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI); rock.castShadow=true; rock.receiveShadow=true; return rock; }
229
- function createFencePost() { const base=createObjectBase("Fence Post"); const mat=new THREE.MeshStandardMaterial({color:0xdeb887,roughness:0.9}); const post=new THREE.Mesh(new THREE.BoxGeometry(0.2,1.5,0.2),mat); Object.assign(post,base); post.position.y=0.75; post.castShadow=true; post.receiveShadow=true; return post; }
230
-
231
-
232
- // --- Session Storage for Local Unsaved Changes ---
233
- function saveUnsavedState() {
234
- try {
235
- // Only save objects marked as newly placed
236
- const stateToSave = newlyPlacedObjects.map(obj => {
237
- if (!obj.userData || !obj.userData.type || !obj.userData.obj_id) return null;
238
- // Make sure we have the necessary info even if object creation failed partially
239
- if (!obj.position || !obj.rotation) return null;
240
- return {
241
- obj_id: obj.userData.obj_id, type: obj.userData.type,
242
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
243
- rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
244
- };
245
- }).filter(obj => obj !== null); // Filter out any nulls from mapping failures
246
-
247
- sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(stateToSave));
248
- console.log(`Saved ${stateToSave.length} unsaved objects to sessionStorage.`);
249
- } catch (e) {
250
- console.error("Error saving state to sessionStorage:", e);
251
- // Avoid breaking the app if sessionStorage fails (e.g., storage full, security settings)
252
  }
 
253
  }
254
 
255
- function restoreUnsavedState() {
256
- try {
257
- const savedState = sessionStorage.getItem(SESSION_STORAGE_KEY);
258
- if (savedState) {
259
- console.log("Found unsaved state in sessionStorage. Restoring...");
260
- const objectsToRestore = JSON.parse(savedState);
261
- if (Array.isArray(objectsToRestore)) {
262
- let count = 0;
263
- objectsToRestore.forEach(objData => {
264
- // Create object, mark as 'isNewUnsavedObject = true'
265
- const mesh = createAndPlaceObject(objData, true);
266
- if(mesh) count++;
267
- });
268
- console.log(`Restored ${count} unsaved objects from sessionStorage.`);
269
- }
270
- } else {
271
- console.log("No unsaved state found in sessionStorage.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
  } catch (e) {
274
- console.error("Error restoring state from sessionStorage:", e);
275
- sessionStorage.removeItem(SESSION_STORAGE_KEY); // Clear potentially corrupted data
276
  }
277
- }
278
 
279
- function clearUnsavedState() {
280
- try {
281
- sessionStorage.removeItem(SESSION_STORAGE_KEY);
282
- // Clear the tracking array
283
- newlyPlacedObjects = [];
284
- // Optionally reset the flag on meshes that were in the array (though they might be gone after rerun)
285
- // For safety, iterate the map and reset flags? Or rely on full reload. Let's rely on reload.
286
- console.log("Cleared unsaved state from memory and sessionStorage.");
287
- } catch (e) {
288
- console.error("Error clearing sessionStorage:", e);
289
- }
290
  }
291
 
292
 
293
  // --- Event Handlers ---
294
- function onMouseMove(event) {
295
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
296
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
297
  }
298
 
299
  function onDocumentClick(event) {
300
- if (selectedObjectType === "None") return;
301
 
302
  const groundCandidates = Object.values(groundMeshes);
303
  if (groundCandidates.length === 0) return;
@@ -308,149 +470,67 @@
308
  if (intersects.length > 0) {
309
  const intersectPoint = intersects[0].point;
310
 
311
- // Prepare object data with a new unique ID
312
  const newObjData = {
313
- obj_id: THREE.MathUtils.generateUUID(), // New ID for every placement
314
  type: selectedObjectType,
315
- position: { x: intersectPoint.x, y: 0, z: intersectPoint.z }, // Start at y=0 on the ground plane
316
- rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' }
317
  };
318
 
319
- // Adjust Y based on object type (move this logic inside createAndPlaceObject?)
320
- // If object's origin isn't its base, adjust here or in the factory.
321
- // Let's assume factories place objects with base near Y=0 for simplicity.
322
- // Example: If rock center is at 0,0,0 in its local space, need to lift it.
323
- if(selectedObjectType === "Rock") newObjData.position.y = 0.35; // Lift rock slightly
324
- else if (selectedObjectType === "Fence Post") newObjData.position.y = 0; // Origin is likely base
325
- // Add other adjustments if needed
 
 
 
 
 
326
 
327
- const newMesh = createAndPlaceObject(newObjData, true); // true = is newly placed & unsaved
328
 
329
- if (newMesh) {
330
- saveUnsavedState(); // Save to sessionStorage immediately after placing
331
- }
332
- }
333
- }
334
 
335
- function onKeyDown(event) { keysPressed[event.code] = true; }
336
- function onKeyUp(event) { keysPressed[event.code] = false; }
337
- function onWindowResize() {
338
- camera.aspect = window.innerWidth / window.innerHeight;
339
- camera.updateProjectionMatrix();
340
- renderer.setSize(window.innerWidth, window.innerHeight);
341
- }
342
 
343
- // --- Functions called by Python ---
344
- function teleportPlayer(targetX, targetZ) {
345
- console.log(`JS teleportPlayer called: x=${targetX}, z=${targetZ}`);
346
- if (playerMesh) {
347
- playerMesh.position.x = targetX;
348
- playerMesh.position.z = targetZ;
349
- const offset = new THREE.Vector3(0, 15, 20);
350
- const targetPosition = playerMesh.position.clone().add(offset);
351
- camera.position.copy(targetPosition);
352
- camera.lookAt(playerMesh.position);
353
- console.log("Player teleported to:", playerMesh.position);
354
- } else { console.error("Player mesh not found for teleport."); }
355
  }
356
 
357
- // Called by Python's save button
358
- function getSaveDataAndPosition() {
359
- console.log(`JS getSaveDataAndPosition called. Found ${newlyPlacedObjects.length} new local objects.`);
360
- // Prepare data for the objects marked as 'newlyPlaced'
361
- const objectsToSave = newlyPlacedObjects.map(obj => {
362
- if (!obj.userData || !obj.userData.type || !obj.userData.obj_id) return null;
363
- return {
364
- obj_id: obj.userData.obj_id, type: obj.userData.type,
365
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
366
- rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
367
- };
368
- }).filter(obj => obj !== null); // Filter out any nulls
369
-
370
- const playerPos = playerMesh ? { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z } : {x:0, y:0, z:0};
371
-
372
- const payload = {
373
- playerPosition: playerPos,
374
- objectsToSave: objectsToSave // This list contains only the locally added, unsaved objects
375
- };
376
- console.log("Prepared payload for saving:", payload);
377
- return JSON.stringify(payload); // Return as JSON string
378
- }
379
 
380
- // Called by Python AFTER successful save and merge
381
- function resetNewlyPlacedObjects() {
382
- console.log(`JS resetNewlyPlacedObjects called.`);
383
- // Clear the tracking array AND the sessionStorage backup
384
- clearUnsavedState();
385
- // The objects themselves remain in the scene until the next Python rerun/refresh
386
- // At which point they will be loaded as part of ALL_INITIAL_OBJECTS
387
  }
388
-
389
- // Called by Python when the selectbox changes
390
- function updateSelectedObjectType(newType) {
391
  console.log("JS updateSelectedObjectType received:", newType);
392
  selectedObjectType = newType;
393
- }
394
-
395
-
396
- // --- Animation Loop ---
397
- function updatePlayerMovement() {
398
- if (!playerMesh) return;
399
- const moveDirection = new THREE.Vector3(0, 0, 0);
400
- if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
401
- if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
402
- if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
403
- if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
404
-
405
- if (moveDirection.lengthSq() > 0) {
406
- const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize();
407
- const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
408
- const worldMove = new THREE.Vector3();
409
- worldMove.add(forward.multiplyScalar(-moveDirection.z));
410
- worldMove.add(right.multiplyScalar(-moveDirection.x));
411
- worldMove.normalize().multiplyScalar(playerSpeed);
412
-
413
- playerMesh.position.add(worldMove);
414
- playerMesh.position.y = Math.max(playerMesh.position.y, 0.8); // Ground clamp
415
-
416
- // Check if we need to create *placeholder* ground nearby
417
- checkAndExpandGroundVisuals();
418
- }
419
  }
420
 
421
- // Create visual placeholder ground dynamically
422
- function checkAndExpandGroundVisuals() {
423
- if (!playerMesh) return;
424
- const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
425
- const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
426
- const viewDistanceGrids = 2; // How many grids away to check/create placeholders
427
-
428
- for (let dx = -viewDistanceGrids; dx <= viewDistanceGrids; dx++) {
429
- for (let dz = -viewDistanceGrids; dz <= viewDistanceGrids; dz++) {
430
- const checkX = currentGridX + dx;
431
- const checkZ = currentGridZ + dz;
432
- const gridKey = `${checkX}_${checkZ}`;
433
-
434
- if (!groundMeshes[gridKey]) {
435
- const isSaved = plotsMetadata.some(p => p.grid_x === checkX && p.grid_z === checkZ);
436
- if (!isSaved) {
437
- createGroundPlane(checkX, checkZ, true);
438
- }
439
- // If it IS saved but missing, Python rerun should inject it later
440
- }
441
- }
442
- }
443
- // Optional: Remove distant placeholders
444
  }
445
-
446
-
447
- function updateCamera() {
448
- if (!playerMesh) return;
449
- const offset = new THREE.Vector3(0, 12, 18);
450
- const targetPosition = playerMesh.position.clone().add(offset);
451
- camera.position.lerp(targetPosition, 0.08);
452
- const lookAtTarget = playerMesh.position.clone().add(new THREE.Vector3(0, 0.5, 0));
453
- camera.lookAt(lookAtTarget);
454
  }
455
 
456
  function animate() {
 
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
+ <title>Shared World Builder</title>
5
  <style>
6
  body { margin: 0; overflow: hidden; }
7
  canvas { display: block; }
 
25
  const keysPressed = {};
26
  const playerSpeed = 0.15;
27
 
28
+ // --- World State (Managed by Server via WebSocket) ---
29
+ const worldObjects = new Map(); // Map<obj_id, THREE.Object3D>
30
+ const groundMeshes = {}; // Map<gridKey, THREE.Mesh>
 
31
 
32
+ // --- WebSocket ---
33
+ let socket = null;
34
+ let connectionRetries = 0;
35
+ const MAX_RETRIES = 5;
36
 
37
+ // --- Access State from Streamlit (Injected) ---
38
+ const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`;
39
+ const websocketUrl = window.WEBSOCKET_URL || "ws://localhost:8765";
40
+ let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None"; // Can be updated
41
  const plotWidth = window.PLOT_WIDTH || 50.0;
42
  const plotDepth = window.PLOT_DEPTH || 50.0;
43
 
44
+ // --- Materials ---
45
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
46
+ const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
47
+ // Basic material cache/reuse
48
+ const objectMaterials = {
49
+ 'wood': new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }),
50
+ 'leaf': new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }),
51
+ 'stone': new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 }),
52
+ 'house_wall': new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 }),
53
+ 'house_roof': new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 }),
54
+ 'brick': new THREE.MeshStandardMaterial({ color: 0x9B4C43, roughness: 0.85 }),
55
+ 'metal': new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.4, metalness: 0.8 }),
56
+ 'gem': new THREE.MeshStandardMaterial({ color: 0x4FFFFF, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.8 }),
57
+ 'light': new THREE.MeshBasicMaterial({ color: 0xFFFF88 }), // For light sources
58
+ // Add more reusable materials
59
+ };
60
 
61
 
62
  function init() {
 
64
  scene.background = new THREE.Color(0xabcdef);
65
 
66
  const aspect = window.innerWidth / window.innerHeight;
67
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 5000); // Increased far plane
68
  camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
69
  camera.lookAt(plotWidth/2, 0, plotDepth/2);
70
  scene.add(camera);
71
 
72
  setupLighting();
73
+ // Don't setup initial ground here, wait for WebSocket initial state? Or create base?
74
+ createGroundPlane(0, 0, false); // Create the origin ground at least
75
  setupPlayer();
76
 
77
  raycaster = new THREE.Raycaster();
 
83
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
84
  document.body.appendChild(renderer.domElement);
85
 
86
+ // --- Initialize WebSocket Connection ---
87
+ connectWebSocket();
 
 
 
 
 
 
 
 
 
 
88
 
89
  // Event Listeners
90
  document.addEventListener('mousemove', onMouseMove, false);
 
95
 
96
  // --- Define global functions needed by Python ---
97
  window.teleportPlayer = teleportPlayer;
98
+ // Removed getSaveDataAndPosition - saving now server-side via WS
99
+ // Removed resetNewlyPlacedObjects - no longer needed
100
+ window.updateSelectedObjectType = updateSelectedObjectType; // Still needed
101
 
102
+ console.log(`Three.js Initialized for user: ${myUsername}. Connecting to ${websocketUrl}...`);
103
  animate();
104
  }
105
 
106
+ // --- WebSocket Logic ---
107
+ function connectWebSocket() {
108
+ console.log("Attempting WebSocket connection...");
109
+ socket = new WebSocket(websocketUrl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ socket.onopen = () => {
112
+ console.log("WebSocket connection established.");
113
+ connectionRetries = 0;
114
+ // Request initial state? Server sends it automatically now.
115
+ // socket.send(JSON.stringify({ type: "request_initial_state" }));
116
+ };
 
 
 
 
 
117
 
118
+ socket.onmessage = (event) => {
119
+ try {
120
+ const data = JSON.parse(event.data);
121
+ // console.log("WebSocket message received:", data); // Debugging
122
+ handleWebSocketMessage(data);
123
+ } catch (e) {
124
+ console.error("Failed to parse WebSocket message:", event.data, e);
125
+ }
126
+ };
 
 
 
 
 
 
 
 
 
 
127
 
128
+ socket.onerror = (error) => {
129
+ console.error("WebSocket error:", error);
130
+ // Consider showing an error message to the user in Streamlit?
131
+ };
132
+
133
+ socket.onclose = (event) => {
134
+ console.warn(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}. Clean: ${event.wasClean}`);
135
+ socket = null;
136
+ // Implement reconnection strategy
137
+ if (connectionRetries < MAX_RETRIES) {
138
+ connectionRetries++;
139
+ const delay = Math.pow(2, connectionRetries) * 1000; // Exponential backoff
140
+ console.log(`Attempting reconnection in ${delay / 1000}s...`);
141
+ setTimeout(connectWebSocket, delay);
142
+ } else {
143
+ console.error("WebSocket reconnection failed after max retries.");
144
+ // Inform user connection lost - maybe via Streamlit call?
145
+ }
146
+ };
147
  }
148
 
149
+ function sendWebSocketMessage(type, payload) {
150
+ if (socket && socket.readyState === WebSocket.OPEN) {
151
+ const message = JSON.stringify({ type, payload });
152
+ socket.send(message);
153
+ } else {
154
+ console.warn("WebSocket not open. Message not sent:", type, payload);
155
+ // Optionally queue messages to send on reconnect?
156
  }
157
+ }
 
 
 
 
 
 
 
 
 
 
158
 
159
+ function handleWebSocketMessage(data) {
160
+ const { type, payload } = data;
161
 
162
+ switch (type) {
163
+ case "initial_state":
164
+ console.log(`Received initial world state with ${Object.keys(payload).length} objects.`);
165
+ // Clear existing objects (except player?) before loading initial state
166
+ clearWorldObjects();
167
+ for (const obj_id in payload) {
168
+ createAndPlaceObject(payload[obj_id], false); // false = not newly placed
169
+ }
170
+ // Setup ground based on loaded objects' positions? Or separate metadata needed?
171
+ // For now, rely on dynamic ground expansion.
172
+ break;
173
+ case "object_placed":
174
+ console.log(`Object placed by ${payload.username}:`, payload.object_data);
175
+ // Add or update the object in the scene
176
+ createAndPlaceObject(payload.object_data, false); // false = not newly placed by *this* client
177
+ break;
178
+ case "object_deleted":
179
+ console.log(`Object deleted by ${payload.username}:`, payload.obj_id);
180
+ removeObjectById(payload.obj_id);
181
+ break;
182
+ case "user_join":
183
+ console.log(`User joined: ${payload.username} (${payload.id})`);
184
+ // Optionally display user join message in chat tab or 3D world?
185
+ break;
186
+ case "user_leave":
187
+ console.log(`User left: ${payload.username} (${payload.id})`);
188
+ // Optionally display user leave message
189
+ break;
190
+ case "user_rename":
191
+ console.log(`User ${payload.old_username} is now ${payload.new_username}`);
192
+ break;
193
+ case "chat_message":
194
+ console.log(`Chat from ${payload.username}: ${payload.message}`);
195
+ // Handle displaying chat in the Streamlit Chat tab (Python side handles this)
196
+ break;
197
+ // Add handlers for other message types
198
+ default:
199
+ console.warn("Received unknown WebSocket message type:", type);
200
  }
201
+ }
202
 
203
+ function clearWorldObjects() {
204
+ console.log("Clearing existing world objects...");
205
+ for (const [obj_id, mesh] of worldObjects.entries()) {
206
+ scene.remove(mesh);
207
+ // Optional: Dispose geometry/material for memory management
208
+ // disposeObject3D(mesh);
209
+ }
210
+ worldObjects.clear();
211
+ // Also clear ground meshes? Or keep them? Keep for now.
212
+ }
213
 
214
+ function removeObjectById(obj_id) {
215
+ if (worldObjects.has(obj_id)) {
216
+ const mesh = worldObjects.get(obj_id);
217
+ scene.remove(mesh);
218
+ // disposeObject3D(mesh); // Optional cleanup
219
+ worldObjects.delete(obj_id);
220
+ console.log(`Removed object ${obj_id} from scene.`);
221
+ } else {
222
+ console.warn(`Attempted to remove non-existent object ID: ${obj_id}`);
223
+ }
224
+ }
225
+
226
+ // --- Standard Setup Functions ---
227
+ function setupLighting() { /* ... (Keep as before) ... */
228
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight);
229
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); directionalLight.position.set(75, 150, 100); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 10; directionalLight.shadow.camera.far = 400; directionalLight.shadow.camera.left = -150; directionalLight.shadow.camera.right = 150; directionalLight.shadow.camera.top = 150; directionalLight.shadow.camera.bottom = -150; directionalLight.shadow.bias = -0.002; scene.add(directionalLight);
230
+ const hemiLight = new THREE.HemisphereLight( 0xabcdef, 0x55aa55, 0.5 ); scene.add( hemiLight );
231
+ }
232
+ function setupPlayer() { /* ... (Keep as before) ... */
233
+ const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8); const playerMat = new THREE.MeshStandardMaterial({ color: 0x0055ff, roughness: 0.6 }); playerMesh = new THREE.Mesh(playerGeo, playerMat); playerMesh.position.set(plotWidth / 2, 0.8, plotDepth / 2); playerMesh.castShadow = true; playerMesh.receiveShadow = false; scene.add(playerMesh);
234
+ }
235
+ function createGroundPlane(gridX, gridZ, isPlaceholder) { /* ... (Keep as before) ... */
236
+ const gridKey = `${gridX}_${gridZ}`; if (groundMeshes[gridKey]) return groundMeshes[gridKey];
237
+ const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth); const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial; const groundMesh = new THREE.Mesh(groundGeometry, material); groundMesh.rotation.x = -Math.PI / 2; groundMesh.position.y = -0.05; groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0; groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0; groundMesh.receiveShadow = true; groundMesh.userData.gridKey = gridKey; groundMesh.userData.isPlaceholder = isPlaceholder; scene.add(groundMesh); groundMeshes[gridKey] = groundMesh; return groundMesh;
238
+ }
239
 
240
+ // --- Object Creation & Placement (Modified for WebSocket & New Primitives) ---
 
241
 
242
+ // Central function to add/update objects based on data
243
+ function createAndPlaceObject(objData, isNewlyPlacedLocally) { // isNewlyPlacedLocally not really used now
244
+ if (!objData || !objData.obj_id || !objData.type) {
245
+ console.warn("Invalid object data:", objData);
246
+ return null;
247
  }
 
 
248
 
249
+ // Check if object already exists (update vs create)
250
+ let mesh = worldObjects.get(objData.obj_id);
251
+ let isNew = false;
252
 
253
+ if (mesh) {
254
+ // Update existing mesh position/rotation if different
255
+ if (mesh.position.distanceToSquared(objData.position) > 0.001) {
256
+ mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
257
+ }
258
+ if (objData.rotation && (
259
+ Math.abs(mesh.rotation.x - objData.rotation._x) > 0.01 ||
260
+ Math.abs(mesh.rotation.y - objData.rotation._y) > 0.01 ||
261
+ Math.abs(mesh.rotation.z - objData.rotation._z) > 0.01 )) {
262
+ mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
263
+ }
264
+ // Could add logic here to update geometry/material if type changes? Unlikely.
265
+ // console.log(`Updated object ${objData.obj_id}`);
266
+ } else {
267
+ // Create new mesh
268
+ mesh = createPrimitiveMesh(objData.type); // Use the new factory function
269
+ if (!mesh) return null; // Failed to create mesh type
270
+
271
+ isNew = true;
272
+ mesh.userData.obj_id = objData.obj_id; // Assign ID from data
273
+ mesh.userData.type = objData.type;
274
+ mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
275
+ if (objData.rotation) {
276
+ mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
277
+ }
278
+
279
+ scene.add(mesh);
280
+ worldObjects.set(objData.obj_id, mesh); // Add to our map
281
+ // console.log(`Created new object ${objData.obj_id} (${objData.type})`);
 
282
  }
283
+ return mesh;
284
  }
285
 
286
+ // Factory function for creating meshes based on type name
287
+ function createPrimitiveMesh(type) {
288
+ let mesh = null;
289
+ let geometry, material, material2; // Declare vars
290
+
291
+ // Use reusable materials where possible
292
+ const wood = objectMaterials.wood;
293
+ const leaf = objectMaterials.leaf;
294
+ const stone = objectMaterials.stone;
295
+ const house_wall = objectMaterials.house_wall;
296
+ const house_roof = objectMaterials.house_roof;
297
+ const brick = objectMaterials.brick;
298
+ const metal = objectMaterials.metal;
299
+ const gem = objectMaterials.gem;
300
+ const lightMat = objectMaterials.light;
301
+
302
+ try { // Wrap in try-catch for safety if geometry fails
303
+ switch(type) {
304
+ // --- Original Primitives ---
305
+ case "Tree":
306
+ mesh = new THREE.Group();
307
+ geometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8); material = wood;
308
+ const trunk = new THREE.Mesh(geometry, material); trunk.position.y = 1; trunk.castShadow=true; trunk.receiveShadow=true; mesh.add(trunk);
309
+ geometry = new THREE.IcosahedronGeometry(1.2, 0); material = leaf;
310
+ const canopy = new THREE.Mesh(geometry, material); canopy.position.y = 2.8; canopy.castShadow=true; canopy.receiveShadow=false; mesh.add(canopy);
311
+ break;
312
+ case "Rock":
313
+ geometry = new THREE.IcosahedronGeometry(0.7, 1); material = stone;
314
+ // Optional: Deform geometry slightly (can be slow if done often)
315
+ mesh = new THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true;
316
+ mesh.scale.set(1, Math.random()*0.4 + 0.8, 1); // Vary shape slightly
317
+ break;
318
+ case "Simple House":
319
+ mesh = new THREE.Group();
320
+ geometry = new THREE.BoxGeometry(2, 1.5, 2.5); material = house_wall;
321
+ const body = new THREE.Mesh(geometry, material); body.position.y = 0.75; body.castShadow = true; body.receiveShadow = true; mesh.add(body);
322
+ geometry = new THREE.ConeGeometry(1.8, 1, 4); material = house_roof;
323
+ const roof = new THREE.Mesh(geometry, material); roof.position.y = 1.5 + 0.5; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = false; mesh.add(roof);
324
+ break;
325
+ case "Fence Post": // Keep original simple fence post
326
+ geometry = new THREE.BoxGeometry(0.2, 1.5, 0.2); material = wood;
327
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true;
328
+ break;
329
+
330
+ // --- New Primitives ---
331
+ case "Pine Tree": // Example: Cone for canopy
332
+ mesh = new THREE.Group();
333
+ geometry = new THREE.CylinderGeometry(0.2, 0.3, 2.5, 8); material = wood;
334
+ const pineTrunk = new THREE.Mesh(geometry, material); pineTrunk.position.y = 1.25; pineTrunk.castShadow=true; pineTrunk.receiveShadow=true; mesh.add(pineTrunk);
335
+ geometry = new THREE.ConeGeometry(1, 2.5, 8); material = leaf;
336
+ const pineCanopy = new THREE.Mesh(geometry, material); pineCanopy.position.y = 2.5 + (2.5/2) - 0.5; pineCanopy.castShadow=true; pineCanopy.receiveShadow=false; mesh.add(pineCanopy);
337
+ break;
338
+ case "Brick Wall": // Simple box with brick color
339
+ geometry = new THREE.BoxGeometry(3, 2, 0.3); material = brick;
340
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1; mesh.castShadow = true; mesh.receiveShadow = true;
341
+ break;
342
+ case "Sphere":
343
+ geometry = new THREE.SphereGeometry(0.8, 16, 12); material = metal;
344
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.8; mesh.castShadow = true; mesh.receiveShadow = true;
345
+ break;
346
+ case "Cube": // Simple cube
347
+ geometry = new THREE.BoxGeometry(1, 1, 1); material = stone; // Re-use stone
348
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.5; mesh.castShadow = true; mesh.receiveShadow = true;
349
+ break;
350
+ case "Cylinder":
351
+ geometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16); material = metal;
352
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true;
353
+ break;
354
+ case "Cone":
355
+ geometry = new THREE.ConeGeometry(0.6, 1.2, 16); material = house_roof; // Re-use roof
356
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true;
357
+ break;
358
+ case "Torus": // Donut shape
359
+ geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24); material = gem; // Use gem material
360
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.7; mesh.castShadow = true; mesh.receiveShadow = true;
361
+ mesh.rotation.x = Math.PI / 2; // Stand it up
362
+ break;
363
+ case "Mushroom":
364
+ mesh = new THREE.Group();
365
+ geometry = new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8); material = house_wall; // Cream stem
366
+ const stem = new THREE.Mesh(geometry, material); stem.position.y = 0.3; stem.castShadow = true; stem.receiveShadow = true; mesh.add(stem);
367
+ geometry = new THREE.SphereGeometry(0.4, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2); material = house_roof; // Red cap
368
+ const cap = new THREE.Mesh(geometry, material); cap.position.y = 0.6; cap.castShadow = true; cap.receiveShadow = false; mesh.add(cap);
369
+ break;
370
+ case "Cactus": // Simple segmented cactus
371
+ mesh = new THREE.Group(); material = leaf; // Green material
372
+ geometry = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8);
373
+ const main = new THREE.Mesh(geometry, material); main.position.y = 0.75; main.castShadow = true; main.receiveShadow = true; mesh.add(main);
374
+ geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 8);
375
+ const arm1 = new THREE.Mesh(geometry, material); arm1.position.set(0.3, 1, 0); arm1.rotation.z = Math.PI / 4; arm1.castShadow = true; arm1.receiveShadow = true; mesh.add(arm1);
376
+ const arm2 = new THREE.Mesh(geometry, material); arm2.position.set(-0.3, 0.8, 0); arm2.rotation.z = -Math.PI / 4; arm2.castShadow = true; arm2.receiveShadow = true; mesh.add(arm2);
377
+ break;
378
+ case "Campfire":
379
+ mesh = new THREE.Group();
380
+ material = wood; geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 5);
381
+ const log1 = new THREE.Mesh(geometry, material); log1.rotation.x = Math.PI/2; log1.position.set(0, 0.1, 0.2); mesh.add(log1);
382
+ const log2 = new THREE.Mesh(geometry, material); log2.rotation.set(Math.PI/2, 0, Math.PI/3); log2.position.set(0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6)); mesh.add(log2);
383
+ const log3 = new THREE.Mesh(geometry, material); log3.rotation.set(Math.PI/2, 0, -Math.PI/3); log3.position.set(-0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6)); mesh.add(log3);
384
+ material2 = lightMat; geometry = new THREE.ConeGeometry(0.2, 0.5, 8); // Simple flame
385
+ const flame = new THREE.Mesh(geometry, material2); flame.position.y = 0.35; mesh.add(flame);
386
+ // Add shadows later if needed
387
+ break;
388
+ case "Star":
389
+ geometry = new THREE.SphereGeometry(0.5, 4, 2); // Low poly sphere looks star-like
390
+ material = lightMat;
391
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1;
392
+ break;
393
+ case "Gem":
394
+ geometry = new THREE.OctahedronGeometry(0.6, 0); material = gem;
395
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true;
396
+ break;
397
+ case "Tower": // Simple cylinder tower
398
+ geometry = new THREE.CylinderGeometry(1, 1.2, 5, 8); material = stone;
399
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 2.5; mesh.castShadow = true; mesh.receiveShadow = true;
400
+ break;
401
+ case "Barrier": // Simple box barrier
402
+ geometry = new THREE.BoxGeometry(2, 0.5, 0.5); material = metal;
403
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.25; mesh.castShadow = true; mesh.receiveShadow = true;
404
+ break;
405
+ case "Fountain": // Placeholder: Tiered cylinders
406
+ mesh = new THREE.Group(); material = stone;
407
+ geometry = new THREE.CylinderGeometry(1.5, 1.5, 0.3, 16);
408
+ const baseF = new THREE.Mesh(geometry, material); baseF.position.y = 0.15; mesh.add(baseF);
409
+ geometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16);
410
+ const midF = new THREE.Mesh(geometry, material); midF.position.y = 0.3+0.25; mesh.add(midF);
411
+ geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.7, 16);
412
+ const topF = new THREE.Mesh(geometry, material); topF.position.y = 0.8+0.35; mesh.add(topF);
413
+ mesh.castShadow = true; mesh.receiveShadow = true; // Apply to group?
414
+ break;
415
+ case "Lantern":
416
+ mesh = new THREE.Group(); material = metal;
417
+ geometry = new THREE.BoxGeometry(0.4, 0.6, 0.4);
418
+ const bodyL = new THREE.Mesh(geometry, material); bodyL.position.y = 0.3; mesh.add(bodyL);
419
+ geometry = new THREE.SphereGeometry(0.15); material2 = lightMat;
420
+ const lightL = new THREE.Mesh(geometry, material2); lightL.position.y = 0.3; mesh.add(lightL);
421
+ mesh.castShadow = true; // Group casts shadow?
422
+ break;
423
+ case "Sign Post":
424
+ mesh = new THREE.Group(); material = wood;
425
+ geometry = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 8);
426
+ const postS = new THREE.Mesh(geometry, material); postS.position.y = 0.9; mesh.add(postS);
427
+ geometry = new THREE.BoxGeometry(0.8, 0.4, 0.05);
428
+ const signS = new THREE.Mesh(geometry, material); signS.position.y = 1.5; mesh.add(signS);
429
+ mesh.castShadow = true; mesh.receiveShadow = true;
430
+ break;
431
+
432
+
433
+ default:
434
+ console.warn("Unknown primitive type for mesh creation:", type);
435
+ return null; // Return null if type not found
436
  }
437
  } catch (e) {
438
+ console.error(`Error creating geometry/mesh for type ${type}:`, e);
439
+ return null;
440
  }
 
441
 
442
+ // Common post-creation steps (if mesh created)
443
+ if (mesh) {
444
+ // Set default userData structure (will be overwritten by createAndPlaceObject)
445
+ mesh.userData = { type: type };
446
+ // Ensure position is defaulted reasonably if created standalone
447
+ if (!mesh.position.y && mesh.geometry) {
448
+ mesh.geometry.computeBoundingBox();
449
+ mesh.position.y = (mesh.geometry.boundingBox.max.y - mesh.geometry.boundingBox.min.y) / 2;
450
+ }
451
+ }
452
+ return mesh;
453
  }
454
 
455
 
456
  // --- Event Handlers ---
457
+ function onMouseMove(event) { /* ... (Keep as before) ... */
458
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
 
459
  }
460
 
461
  function onDocumentClick(event) {
462
+ if (selectedObjectType === "None" || !selectedObjectType) return;
463
 
464
  const groundCandidates = Object.values(groundMeshes);
465
  if (groundCandidates.length === 0) return;
 
470
  if (intersects.length > 0) {
471
  const intersectPoint = intersects[0].point;
472
 
473
+ // Prepare object data for the server
474
  const newObjData = {
475
+ obj_id: THREE.MathUtils.generateUUID(), // Generate unique ID client-side
476
  type: selectedObjectType,
477
+ position: { x: intersectPoint.x, y: 0, z: intersectPoint.z }, // Base position on ground
478
+ rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' } // Random Y rotation
479
  };
480
 
481
+ // Adjust Y position based on object type AFTER getting the type
482
+ // This should ideally use the geometry's bounding box, but hardcoding for now
483
+ const tempMesh = createPrimitiveMesh(selectedObjectType); // Create temporarily to get height? Costly.
484
+ if (tempMesh && tempMesh.geometry) {
485
+ tempMesh.geometry.computeBoundingBox();
486
+ const height = tempMesh.geometry.boundingBox.max.y - tempMesh.geometry.boundingBox.min.y;
487
+ // Assume origin is at the center Y for most default geometries
488
+ newObjData.position.y = (height / 2) + intersectPoint.y + 0.01; // Place base slightly above ground
489
+ } else {
490
+ // Fallback if mesh creation failed or no geometry
491
+ newObjData.position.y = 0.5 + intersectPoint.y; // Default lift
492
+ }
493
 
494
+ console.log(`Placing ${selectedObjectType} (${newObjData.obj_id}) at`, newObjData.position);
495
 
496
+ // 1. Add object visually immediately (Optimistic Update)
497
+ createAndPlaceObject(newObjData, true); // Mark as locally placed initially? Not needed now.
 
 
 
498
 
499
+ // 2. Send placement message to server via WebSocket
500
+ sendWebSocketMessage("place_object", {
501
+ username: myUsername,
502
+ object_data: newObjData
503
+ });
 
 
504
 
505
+ // 3. No need for local saving (sessionStorage) anymore
506
+ }
 
 
 
 
 
 
 
 
 
 
507
  }
508
 
509
+ function onKeyDown(event) { /* ... (Keep as before) ... */ keysPressed[event.code] = true; }
510
+ function onKeyUp(event) { /* ... (Keep as before) ... */ keysPressed[event.code] = false; }
511
+ function onWindowResize() { /* ... (Keep as before) ... */ camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
+ // --- Functions called by Python ---
514
+ function teleportPlayer(targetX, targetZ) { /* ... (Keep as before) ... */
515
+ console.log(`JS teleportPlayer called: x=${targetX}, z=${targetZ}`); if (playerMesh) { playerMesh.position.x = targetX; playerMesh.position.z = targetZ; const offset = new THREE.Vector3(0, 15, 20); const targetPosition = playerMesh.position.clone().add(offset); camera.position.copy(targetPosition); camera.lookAt(playerMesh.position); console.log("Player teleported to:", playerMesh.position); } else { console.error("Player mesh not found for teleport."); }
 
 
 
 
516
  }
517
+ function updateSelectedObjectType(newType) { // Renamed from previous attempt
 
 
518
  console.log("JS updateSelectedObjectType received:", newType);
519
  selectedObjectType = newType;
520
+ // Optionally provide visual feedback (e.g., change cursor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  }
522
 
523
+ // --- Animation Loop & Helpers ---
524
+ function updatePlayerMovement() { /* ... (Keep as before, includes checkAndExpandGroundVisuals) ... */
525
+ if (!playerMesh) return; const moveDirection = new THREE.Vector3(0, 0, 0); if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1; if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1; if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1; if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
526
+ if (moveDirection.lengthSq() > 0) { const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize(); const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize(); const worldMove = new THREE.Vector3(); worldMove.add(forward.multiplyScalar(-moveDirection.z)); worldMove.add(right.multiplyScalar(-moveDirection.x)); worldMove.normalize().multiplyScalar(playerSpeed); playerMesh.position.add(worldMove); playerMesh.position.y = Math.max(playerMesh.position.y, 0.8); checkAndExpandGroundVisuals(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  }
528
+ function checkAndExpandGroundVisuals() { /* ... (Keep as before) ... */
529
+ if (!playerMesh) return; const currentGridX = Math.floor(playerMesh.position.x / plotWidth); const currentGridZ = Math.floor(playerMesh.position.z / plotDepth); const viewDistanceGrids = 3; // Expand further?
530
+ for (let dx = -viewDistanceGrids; dx <= viewDistanceGrids; dx++) { for (let dz = -viewDistanceGrids; dz <= viewDistanceGrids; dz++) { const checkX = currentGridX + dx; const checkZ = currentGridZ + dz; const gridKey = `${checkX}_${checkZ}`; if (!groundMeshes[gridKey]) { createGroundPlane(checkX, checkZ, true); } } }
531
+ }
532
+ function updateCamera() { /* ... (Keep as before) ... */
533
+ if (!playerMesh) return; const offset = new THREE.Vector3(0, 12, 18); const targetPosition = playerMesh.position.clone().add(offset); camera.position.lerp(targetPosition, 0.08); const lookAtTarget = playerMesh.position.clone().add(new THREE.Vector3(0, 0.5, 0)); camera.lookAt(lookAtTarget);
 
 
 
534
  }
535
 
536
  function animate() {