awacke1 commited on
Commit
6761242
·
verified ·
1 Parent(s): c7e4e3d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +322 -361
index.html CHANGED
@@ -1,13 +1,22 @@
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; }
 
 
 
 
 
 
8
  </style>
9
  </head>
10
  <body>
 
 
 
11
  <script type="importmap">
12
  {
13
  "imports": {
@@ -28,23 +37,25 @@
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 }),
@@ -54,24 +65,28 @@
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() {
 
63
  scene = new THREE.Scene();
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,459 +98,405 @@
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);
91
- document.addEventListener('click', onDocumentClick, false); // Place object
92
  window.addEventListener('resize', onWindowResize, false);
 
 
 
93
  document.addEventListener('keydown', onKeyDown);
94
  document.addEventListener('keyup', onKeyUp);
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;
466
-
467
  raycaster.setFromCamera(mouse, camera);
468
- const intersects = raycaster.intersectObjects(groundCandidates);
469
-
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() {
537
  requestAnimationFrame(animate);
538
- updatePlayerMovement();
539
  updateCamera();
540
  renderer.render(scene, camera);
541
  }
 
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
+ <title>Live World Builder</title>
5
  <style>
6
+ body { margin: 0; overflow: hidden; background-color: #f0f2f6; font-family: sans-serif;}
7
  canvas { display: block; }
8
+ #info { position: absolute; top: 10px; width: 100%; text-align: center; z-index: 100; display:block; color: #333; background-color: rgba(255,255,255,0.7); padding: 5px; border-radius: 5px;}
9
+ #connection-status { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.5); color: white; padding: 3px 8px; border-radius: 3px; font-size: 0.8em; z-index: 100; }
10
+ .dot { height: 10px; width: 10px; border-radius: 50%; display: inline-block; margin-right: 5px; }
11
+ .red { background-color: #f44336; }
12
+ .green { background-color: #4CAF50; }
13
+ .orange { background-color: #ff9800; }
14
  </style>
15
  </head>
16
  <body>
17
+ <div id="info">Builder: <span id="username">User</span> | Tool: <span id="current-tool">None</span> | Click to Place | Right-Click to Delete</div>
18
+ <div id="connection-status"><span id="ws-dot" class="dot red"></span>Disconnected</div>
19
+
20
  <script type="importmap">
21
  {
22
  "imports": {
 
37
  // --- World State (Managed by Server via WebSocket) ---
38
  const worldObjects = new Map(); // Map<obj_id, THREE.Object3D>
39
  const groundMeshes = {}; // Map<gridKey, THREE.Mesh>
40
+ const otherPlayers = new Map(); // Map<client_id, THREE.Mesh> for presence
41
 
42
  // --- WebSocket ---
43
  let socket = null;
44
  let connectionRetries = 0;
45
  const MAX_RETRIES = 5;
46
+ let pingIntervalId = null;
47
 
48
  // --- Access State from Streamlit (Injected) ---
49
  const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`;
50
  const websocketUrl = window.WEBSOCKET_URL || "ws://localhost:8765";
51
+ let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
52
  const plotWidth = window.PLOT_WIDTH || 50.0;
53
  const plotDepth = window.PLOT_DEPTH || 50.0;
54
+ // NOTE: initialWorldObjects from injection is removed, state comes via WebSocket
55
 
56
+ // --- Materials (Reusable) ---
57
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x66aa66, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
58
+ const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x559955, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
 
59
  const objectMaterials = {
60
  'wood': new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }),
61
  'leaf': new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }),
 
65
  'brick': new THREE.MeshStandardMaterial({ color: 0x9B4C43, roughness: 0.85 }),
66
  'metal': new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.4, metalness: 0.8 }),
67
  'gem': new THREE.MeshStandardMaterial({ color: 0x4FFFFF, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.8 }),
68
+ 'light': new THREE.MeshBasicMaterial({ color: 0xFFFF88 }),
69
+ 'cactus': new THREE.MeshStandardMaterial({ color: 0x558B2F, roughness: 0.8 }),
70
+ 'mushroom_stem': new THREE.MeshStandardMaterial({ color: 0xF5F5DC, roughness: 0.9 }),
71
+ 'mushroom_cap': new THREE.MeshStandardMaterial({ color: 0xB22222, roughness: 0.7 }),
72
+ 'player_other': new THREE.MeshStandardMaterial({ color: 0x8888ff, roughness: 0.7 }) // Other players
73
  };
74
 
75
 
76
+ // --- Initialization ---
77
  function init() {
78
+ console.log("JS Init: Starting scene setup...");
79
  scene = new THREE.Scene();
80
+ scene.background = new THREE.Color(0xADD8E6);
81
 
82
  const aspect = window.innerWidth / window.innerHeight;
83
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 5000);
84
  camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
85
  camera.lookAt(plotWidth/2, 0, plotDepth/2);
86
  scene.add(camera);
87
 
88
  setupLighting();
89
+ createGroundPlane(0, 0, false);
 
90
  setupPlayer();
91
 
92
  raycaster = new THREE.Raycaster();
 
98
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
99
  document.body.appendChild(renderer.domElement);
100
 
101
+ connectWebSocket(); // Initialize WebSocket Connection
102
+
103
+ updateInfoPanel(); // Initial UI update
104
 
105
  // Event Listeners
 
 
106
  window.addEventListener('resize', onWindowResize, false);
107
+ renderer.domElement.addEventListener('mousemove', onMouseMove, false);
108
+ renderer.domElement.addEventListener('click', onDocumentClick, false);
109
+ renderer.domElement.addEventListener('contextmenu', onRightClick, false);
110
  document.addEventListener('keydown', onKeyDown);
111
  document.addEventListener('keyup', onKeyUp);
112
 
113
  // --- Define global functions needed by Python ---
114
  window.teleportPlayer = teleportPlayer;
115
+ window.updateSelectedObjectType = updateSelectedObjectType;
116
+ // Removed loadWorldState and getWorldStateForSave as WS handles state now
 
117
 
118
+ console.log(`JS Init: Setup complete for user: ${myUsername}. Connecting to ${websocketUrl}...`);
119
  animate();
120
  }
121
 
122
  // --- WebSocket Logic ---
123
+ function updateConnectionStatus(status) {
124
+ const statusEl = document.getElementById('connection-status');
125
+ const dotEl = document.getElementById('ws-dot');
126
+ if (statusEl && dotEl) {
127
+ let text = 'Disconnected';
128
+ dotEl.className = 'dot red'; // Default red
129
+ if (status === 'connected') { text = 'Connected'; dotEl.className = 'dot green'; }
130
+ else if (status === 'connecting') { text = 'Connecting...'; dotEl.className = 'dot orange'; }
131
+ else if (status === 'error') { text = 'Connection Error'; }
132
+ statusEl.innerHTML = `<span id="ws-dot" class="${dotEl.className}"></span>${text}`;
133
+ }
134
+ }
135
+
136
  function connectWebSocket() {
137
+ console.log(`Attempting WebSocket connection (${connectionRetries + 1}/${MAX_RETRIES})...`);
138
+ updateConnectionStatus('connecting');
139
  socket = new WebSocket(websocketUrl);
140
 
141
  socket.onopen = () => {
142
+ console.log("WebSocket connection established.");
143
+ updateConnectionStatus('connected');
144
  connectionRetries = 0;
145
+ // Start sending pings to keep connection alive
146
+ if(pingIntervalId) clearInterval(pingIntervalId);
147
+ pingIntervalId = setInterval(() => {
148
+ sendWebSocketMessage('ping', {});
149
+ }, 30000); // Send ping every 30 seconds
150
+ // Initial state requested/sent by server handler now
151
  };
152
 
153
  socket.onmessage = (event) => {
154
  try {
155
  const data = JSON.parse(event.data);
 
156
  handleWebSocketMessage(data);
157
+ } catch (e) { console.error("❌ Failed to parse WebSocket message:", event.data, e); }
 
 
158
  };
159
 
160
  socket.onerror = (error) => {
161
+ console.error("WebSocket error:", error);
162
+ updateConnectionStatus('error');
163
  };
164
 
165
  socket.onclose = (event) => {
166
+ console.warn(`🔌 WebSocket connection closed. Code: ${event.code}, Clean: ${event.wasClean}`);
167
+ updateConnectionStatus('disconnected');
168
+ if(pingIntervalId) clearInterval(pingIntervalId);
169
+ pingIntervalId = null;
170
  socket = null;
171
+ if (connectionRetries < MAX_RETRIES) { // Reconnect logic
 
172
  connectionRetries++;
173
+ const delay = Math.pow(2, connectionRetries) * 1000;
174
  console.log(`Attempting reconnection in ${delay / 1000}s...`);
175
+ updateConnectionStatus('connecting');
176
  setTimeout(connectWebSocket, delay);
177
+ } else { console.error("❌ WebSocket reconnection failed after max retries."); }
 
 
 
178
  };
179
  }
180
 
181
+ function sendWebSocketMessage(type, payload = {}) {
182
  if (socket && socket.readyState === WebSocket.OPEN) {
183
+ // Include username in all outgoing messages automatically
184
+ payload.username = myUsername;
185
  const message = JSON.stringify({ type, payload });
186
  socket.send(message);
187
+ } else { console.warn("⚠️ WebSocket not open. Message not sent:", type); }
 
 
 
188
  }
189
 
190
  function handleWebSocketMessage(data) {
191
  const { type, payload } = data;
192
+ // console.log("Received WS message:", type, payload); // Verbose logging
193
 
194
  switch (type) {
195
  case "initial_state":
196
+ console.log(`✨ Received initial world state with ${Object.keys(payload).length} objects.`);
197
+ clearWorldObjects(); // Clear previous objects
198
+ for (const obj_id in payload) { createAndPlaceObject(payload[obj_id]); }
199
+ checkAndExpandGroundVisuals(); // Create initial ground visuals
 
 
 
 
200
  break;
201
  case "object_placed":
202
+ console.log(`➕ Object placed by ${payload.username}: ${payload.object_data?.type}`);
203
+ createAndPlaceObject(payload.object_data); // Add or update
 
204
  break;
205
  case "object_deleted":
206
+ console.log(`➖ Object deleted by ${payload.username}: ${payload.obj_id}`);
207
  removeObjectById(payload.obj_id);
208
  break;
209
+ case "user_join": console.log(`👋 User joined: ${payload.username} (${payload.id})`); break;
 
 
 
210
  case "user_leave":
211
+ console.log(`🚪 User left: ${payload.username} (${payload.id})`);
212
+ removeOtherPlayer(payload.id); // Remove visual representation
213
  break;
214
+ case "player_moved": // Update visual representation of other players
215
+ if(payload.id !== socket?.id) { // Don't update self visually
216
+ updateOtherPlayer(payload.id, payload.username, payload.position, payload.rotation);
217
+ }
218
  break;
219
+ case "pong": break; // Ignore server pong reply
220
+ default: console.warn("⚠️ Received unknown WebSocket message type:", type);
 
 
 
 
 
221
  }
222
  }
223
 
224
+ // --- Other Player Visuals ---
225
+ function addOrUpdateOtherPlayerMesh(id, username) {
226
+ let playerViz = otherPlayers.get(id);
227
+ if (!playerViz) {
228
+ const geometry = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
229
+ const material = objectMaterials.player_other;
230
+ playerViz = new THREE.Mesh(geometry, material);
231
+ playerViz.name = `player_${username}_${id.substring(0,4)}`;
232
+ playerViz.castShadow = true;
233
+ otherPlayers.set(id, playerViz);
234
+ scene.add(playerViz);
235
+ console.log(`Added visual for player ${username} (${id})`);
236
  }
237
+ return playerViz;
 
238
  }
239
+ function updateOtherPlayer(id, username, position, rotation) {
240
+ const playerViz = addOrUpdateOtherPlayerMesh(id, username);
241
+ if(position) playerViz.position.set(position.x, position.y, position.z);
242
+ if(rotation) playerViz.rotation.set(rotation._x, rotation._y, rotation._z, rotation._order || 'XYZ');
243
+ }
244
+ function removeOtherPlayer(id) {
245
+ if (otherPlayers.has(id)) {
246
+ const mesh = otherPlayers.get(id);
247
  scene.remove(mesh);
248
+ if(mesh.geometry) mesh.geometry.dispose();
249
+ otherPlayers.delete(id);
250
+ console.log(`Removed visual for player ${id}`);
 
 
251
  }
252
  }
253
 
254
+
255
  // --- Standard Setup Functions ---
256
+ function setupLighting() {
257
+ scene.add(new THREE.AmbientLight(0xffffff, 0.7));
258
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
259
+ dirLight.position.set(80, 150, 100); dirLight.castShadow = true;
260
+ dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048;
261
+ dirLight.shadow.camera.near = 10; dirLight.shadow.camera.far = 400;
262
+ dirLight.shadow.camera.left = -150; dirLight.shadow.camera.right = 150;
263
+ dirLight.shadow.camera.top = 150; dirLight.shadow.camera.bottom = -150;
264
+ dirLight.shadow.bias = -0.002; scene.add(dirLight);
265
+ scene.add(new THREE.HemisphereLight( 0xADD8E6, 0x66aa66, 0.4 ));
266
  }
267
+ function setupPlayer() {
268
+ const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
269
+ const playerMat = new THREE.MeshStandardMaterial({ color: 0xff4400, roughness: 0.6, metalness: 0.1 });
270
+ playerMesh = new THREE.Mesh(playerGeo, playerMat);
271
+ playerMesh.position.set(plotWidth / 2, 0.8, plotDepth / 2);
272
+ playerMesh.castShadow = true; playerMesh.receiveShadow = false;
273
+ playerMesh.name = "player_local"; scene.add(playerMesh);
274
+ }
275
+ function createGroundPlane(gridX, gridZ, isPlaceholder) {
276
  const gridKey = `${gridX}_${gridZ}`; if (groundMeshes[gridKey]) return groundMeshes[gridKey];
277
+ const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
278
+ const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
279
+ const groundMesh = new THREE.Mesh(groundGeometry, material);
280
+ groundMesh.rotation.x = -Math.PI / 2;
281
+ groundMesh.position.set(gridX * plotWidth + plotWidth / 2.0, -0.05, gridZ * plotDepth + plotDepth / 2.0);
282
+ groundMesh.receiveShadow = true; groundMesh.userData = { gridKey: gridKey, isPlaceholder: isPlaceholder };
283
+ groundMesh.name = `ground_${gridKey}`; scene.add(groundMesh); groundMeshes[gridKey] = groundMesh; return groundMesh;
284
  }
285
 
286
+ // --- Object Creation / Management ---
287
+ function clearWorldObjects() {
288
+ console.log("JS: Clearing existing world objects...");
289
+ const idsToRemove = Array.from(worldObjects.keys());
290
+ idsToRemove.forEach(obj_id => removeObjectById(obj_id));
291
+ worldObjects.clear();
292
+ console.log("JS: World objects cleared.");
293
+ }
294
+ function removeObjectById(obj_id) {
295
+ if (worldObjects.has(obj_id)) {
296
+ const mesh = worldObjects.get(obj_id);
297
+ scene.remove(mesh);
298
+ if (mesh.geometry) mesh.geometry.dispose();
299
+ if (mesh.material) { if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose()); else mesh.material.dispose(); }
300
+ worldObjects.delete(obj_id);
301
+ console.log(`JS: Removed object ${obj_id}`);
302
+ }
303
+ }
304
+ function createAndPlaceObject(objData) { // Renamed, removed bool flag
305
+ if (!objData || !objData.obj_id || !objData.type) { console.warn("JS: Invalid object data:", objData); return null; }
306
+ if (worldObjects.has(objData.obj_id)) { // Update existing
307
+ const mesh = worldObjects.get(objData.obj_id);
308
+ if (mesh.position.distanceToSquared(objData.position) > 0.001) { mesh.position.set(objData.position.x, objData.position.y, objData.position.z); }
309
+ if (objData.rotation && (Math.abs(mesh.rotation.x - objData.rotation._x) > 0.01 || Math.abs(mesh.rotation.y - objData.rotation._y) > 0.01 || Math.abs(mesh.rotation.z - objData.rotation._z) > 0.01 )) { mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ'); }
310
+ return mesh;
311
+ } else { // Create new
312
+ let mesh = createPrimitiveMesh(objData.type);
313
+ if (!mesh) return null;
314
+ mesh.userData = { obj_id: objData.obj_id, type: objData.type };
315
+ mesh.name = `${objData.type}_${objData.obj_id.substring(0, 4)}`;
316
+ if (objData.position) { mesh.position.set(objData.position.x, objData.position.y, objData.position.z); }
317
+ if (objData.rotation) { mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ'); }
 
 
 
 
 
 
 
318
  scene.add(mesh);
319
+ worldObjects.set(objData.obj_id, mesh);
320
+ return mesh;
321
  }
 
322
  }
323
+ function createPrimitiveMesh(type) { // Keep factory as before
324
+ let mesh = null; let geometry, material, material2;
325
+ const wood = objectMaterials.wood; const leaf = objectMaterials.leaf; const stone = objectMaterials.stone;
326
+ const house_wall = objectMaterials.house_wall; const house_roof = objectMaterials.house_roof;
327
+ const brick = objectMaterials.brick; const metal = objectMaterials.metal; const gem = objectMaterials.gem;
328
+ const lightMat = objectMaterials.light; const cactus = objectMaterials.cactus;
329
+ const mushroom_stem = objectMaterials.mushroom_stem; const mushroom_cap = objectMaterials.mushroom_cap;
330
+ try {
 
 
 
 
 
 
 
 
 
 
331
  switch(type) {
332
+ case "Tree": mesh = new THREE.Group(); material = wood; material2 = leaf; const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), material); trunk.position.y = 1; trunk.castShadow=true; trunk.receiveShadow=true; mesh.add(trunk); const canopy = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), material2); canopy.position.y = 2.8; canopy.castShadow=true; canopy.receiveShadow=false; mesh.add(canopy); break;
333
+ case "Rock": geometry = new THREE.IcosahedronGeometry(0.7, 1); material = stone; mesh = new THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; mesh.scale.set(1 + Math.random()*0.2-0.1, Math.random()*0.4 + 0.8, 1 + Math.random()*0.2-0.1); mesh.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI); break;
334
+ case "Simple House": mesh = new THREE.Group(); material = house_wall; material2 = house_roof; const body = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), material); body.position.y = 0.75; body.castShadow = true; body.receiveShadow = true; mesh.add(body); const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), material2); roof.position.y = 1.5 + 0.5; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = false; mesh.add(roof); break;
335
+ case "Fence Post": geometry = new THREE.BoxGeometry(0.2, 1.5, 0.2); material = wood; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true; break;
336
+ case "Pine Tree": mesh = new THREE.Group(); material = wood; material2 = leaf; const pineTrunk = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.3, 2.5, 8), material); pineTrunk.position.y = 1.25; pineTrunk.castShadow=true; pineTrunk.receiveShadow=true; mesh.add(pineTrunk); const pineCanopy = new THREE.Mesh(new THREE.ConeGeometry(1, 2.5, 8), material2); pineCanopy.position.y = 2.5; pineCanopy.castShadow=true; pineCanopy.receiveShadow=false; mesh.add(pineCanopy); break;
337
+ case "Brick Wall": geometry = new THREE.BoxGeometry(3, 2, 0.3); material = brick; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1; mesh.castShadow = true; mesh.receiveShadow = true; break;
338
+ case "Sphere": geometry = new THREE.SphereGeometry(0.8, 16, 12); material = metal; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.8; mesh.castShadow = true; mesh.receiveShadow = true; break;
339
+ case "Cube": geometry = new THREE.BoxGeometry(1, 1, 1); material = wood; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.5; mesh.castShadow = true; mesh.receiveShadow = true; break;
340
+ case "Cylinder": geometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16); material = stone; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true; break;
341
+ case "Cone": geometry = new THREE.ConeGeometry(0.6, 1.2, 16); material = leaf; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true; break;
342
+ case "Torus": geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24); material = metal; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.7; mesh.castShadow = true; mesh.receiveShadow = true; mesh.rotation.x = Math.PI / 2; break;
343
+ case "Mushroom": mesh = new THREE.Group(); material = mushroom_stem; material2 = mushroom_cap; const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8), material); stem.position.y = 0.3; stem.castShadow = true; stem.receiveShadow = true; mesh.add(stem); const cap = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 8, 0, Math.PI * 2, 0, Math.PI / 1.8), material2); cap.position.y = 0.6; cap.castShadow = true; cap.receiveShadow = false; mesh.add(cap); break;
344
+ case "Cactus": mesh = new THREE.Group(); material = cactus; geometry = new THREE.CapsuleGeometry(0.3, 1.2, 4, 8); const main = new THREE.Mesh(geometry, material); main.position.y = (1.2/2) + 0.3; main.castShadow = true; main.receiveShadow = true; mesh.add(main); geometry = new THREE.CapsuleGeometry(0.2, 0.6, 4, 8); 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); 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); break;
345
+ case "Campfire": mesh = new THREE.Group(); material = wood; material2 = lightMat; geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 5); const log1 = new THREE.Mesh(geometry, material); log1.rotation.z = Math.PI/2; log1.position.set(0, 0.1, 0.2); log1.castShadow=true; mesh.add(log1); const log2 = new THREE.Mesh(geometry, material); log2.rotation.z = Math.PI/2; log2.rotation.y = Math.PI/3; log2.position.set(0.15, 0.1, -0.15); log2.castShadow=true; mesh.add(log2); const log3 = new THREE.Mesh(geometry, material); log3.rotation.z = Math.PI/2; log3.rotation.y = -Math.PI/3; log3.position.set(-0.15, 0.1, -0.15); log3.castShadow=true; mesh.add(log3); geometry = new THREE.ConeGeometry(0.2, 0.5, 8); const flame = new THREE.Mesh(geometry, material2); flame.position.y = 0.35; mesh.add(flame); break;
346
+ case "Star": geometry = new THREE.SphereGeometry(0.5, 5, 4); material = lightMat; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1; break;
347
+ case "Gem": geometry = new THREE.OctahedronGeometry(0.6, 0); material = gem; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true; break;
348
+ case "Tower": geometry = new THREE.CylinderGeometry(1, 1.2, 5, 8); material = stone; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 2.5; mesh.castShadow = true; mesh.receiveShadow = true; break;
349
+ case "Barrier": geometry = new THREE.BoxGeometry(2, 0.5, 0.5); material = metal; mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.25; mesh.castShadow = true; mesh.receiveShadow = true; break;
350
+ case "Fountain": mesh = new THREE.Group(); material = stone; geometry = new THREE.CylinderGeometry(1.5, 1.5, 0.3, 16); const baseF = new THREE.Mesh(geometry, material); baseF.position.y = 0.15; baseF.receiveShadow=true; mesh.add(baseF); geometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16); const midF = new THREE.Mesh(geometry, material); midF.position.y = 0.3+0.25; midF.castShadow=true; mesh.add(midF); geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.7, 16); const topF = new THREE.Mesh(geometry, material); topF.position.y = 0.8+0.35; topF.castShadow=true; mesh.add(topF); break;
351
+ case "Lantern": mesh = new THREE.Group(); material = metal; material2 = lightMat; geometry = new THREE.BoxGeometry(0.4, 0.6, 0.4); const bodyL = new THREE.Mesh(geometry, material); bodyL.position.y = 0.3; bodyL.castShadow=true; mesh.add(bodyL); geometry = new THREE.SphereGeometry(0.15); const lightL = new THREE.Mesh(geometry, material2); lightL.position.y = 0.3; mesh.add(lightL); break;
352
+ case "Sign Post": mesh = new THREE.Group(); material = wood; geometry = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 8); const postS = new THREE.Mesh(geometry, material); postS.position.y = 0.9; postS.castShadow=true; postS.receiveShadow=true; mesh.add(postS); geometry = new THREE.BoxGeometry(0.8, 0.4, 0.05); const signS = new THREE.Mesh(geometry, material); signS.position.y = 1.5; signS.castShadow=true; signS.receiveShadow=true; mesh.add(signS); break;
353
+ default: console.warn("JS: Unknown primitive type:", type); return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  }
355
+ } catch (e) { console.error(`JS: Error creating mesh for ${type}:`, e); return null; }
356
+ if (mesh) { mesh.userData = { type: type }; } // Set basic userdata
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  return mesh;
358
  }
359
 
360
 
361
  // --- Event Handlers ---
362
+ function onMouseMove(event) {
363
+ const rect = renderer.domElement.getBoundingClientRect();
364
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
365
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
366
  }
367
 
368
+ function onDocumentClick(event) { // Left Click - Place Object
369
+ // console.log("JS Click. Tool:", selectedObjectType);
370
  if (selectedObjectType === "None" || !selectedObjectType) return;
 
 
 
 
371
  raycaster.setFromCamera(mouse, camera);
372
+ const grounds = Object.values(groundMeshes);
373
+ const intersects = raycaster.intersectObjects(grounds);
374
  if (intersects.length > 0) {
375
  const intersectPoint = intersects[0].point;
 
 
376
  const newObjData = {
377
+ obj_id: THREE.MathUtils.generateUUID(), type: selectedObjectType,
378
+ position: { x: intersectPoint.x, y: 0, z: intersectPoint.z },
379
+ rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' }
 
380
  };
381
+ // Adjust Y - Needs refinement based on actual geometry origins/bounding boxes
382
+ let yOffset = 0.5; // Default lift
383
+ if (["Rock", "Sphere", "Gem"].includes(selectedObjectType)) yOffset = 0.4;
384
+ else if (["Simple House", "Cylinder", "Torus", "Fence Post", "Barrier", "Lantern"].includes(selectedObjectType)) yOffset = 0.75;
385
+ else if (["Tree", "Pine Tree", "Mushroom", "Cactus", "Campfire", "Sign Post"].includes(selectedObjectType)) yOffset = 0; // Group base likely at 0
386
+ else if (selectedObjectType === "Tower") yOffset = 2.5;
387
+ else if (selectedObjectType === "Cone") yOffset = 0.6;
388
+ else if (selectedObjectType === "Star") yOffset = 1.0;
389
+ else if (selectedObjectType === "Fountain") yOffset = 0.15;
390
+ newObjData.position.y = intersectPoint.y + yOffset + 0.01; // Add slight buffer
391
+
392
+ // Optimistic local placement (object added by server message now)
393
+ // createAndPlaceObject(newObjData); // Don't add locally first? Or do? Let's add locally for responsiveness.
394
+ const tempMesh = createAndPlaceObject(newObjData); // Add locally & track
395
+ if(tempMesh){
396
+ sendWebSocketMessage("place_object", { object_data: newObjData }); // Send to server
397
  } else {
398
+ console.error("Failed to create mesh locally for placement.")
 
399
  }
400
+ }
401
+ }
402
 
403
+ function onRightClick(event) { // Right Click - Delete Object
404
+ event.preventDefault();
405
+ raycaster.setFromCamera(mouse, camera);
406
+ const objectMeshes = Array.from(worldObjects.values());
407
+ const intersects = raycaster.intersectObjects(objectMeshes, true);
408
+ if (intersects.length > 0) {
409
+ let objectToDelete = intersects[0].object;
410
+ while (objectToDelete.parent && objectToDelete.parent !== scene && objectToDelete.userData?.obj_id === undefined) {
411
+ objectToDelete = objectToDelete.parent; // Find parent with ID
412
+ }
413
+ const objIdToDelete = objectToDelete.userData?.obj_id;
414
+ if (objIdToDelete && worldObjects.has(objIdToDelete)) {
415
+ console.log(`JS Requesting delete for: ${objIdToDelete}`);
416
+ // Optimistic local removal
417
+ removeObjectById(objIdToDelete);
418
+ // Send delete request to server
419
+ sendWebSocketMessage("delete_object", { obj_id: objIdToDelete });
420
+ // Log client-side deletion attempt
421
+ try { streamlit_js_eval(`add_action_log('Requested Delete: ${objIdToDelete.substring(0,6)}...')`, key='js_delete_log'); } catch(e) {}
422
 
423
+ }
424
  }
425
  }
426
 
 
 
 
427
 
428
+ function onKeyDown(event) { keysPressed[event.code] = true; }
429
+ function onKeyUp(event) { keysPressed[event.code] = false; }
430
+ function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
431
+
432
+ // --- Functions callable by Python ---
433
+ function teleportPlayer(targetX, targetZ) {
434
+ console.log(`JS teleportPlayer called: x=${targetX}, z=${targetZ}`);
435
+ if (playerMesh) {
436
+ playerMesh.position.set(targetX, playerMesh.position.y, targetZ);
437
+ const offset = new THREE.Vector3(0, 15, 20);
438
+ const targetPosition = playerMesh.position.clone().add(offset);
439
+ camera.position.copy(targetPosition); camera.lookAt(playerMesh.position);
440
+ }
441
  }
442
+ function updateSelectedObjectType(newType) {
443
  console.log("JS updateSelectedObjectType received:", newType);
444
+ selectedObjectType = newType; updateInfoPanel();
445
+ }
446
+ function updateInfoPanel() { // Helper to update top UI
447
+ const userEl = document.getElementById('username');
448
+ const toolEl = document.getElementById('current-tool');
449
+ if (userEl) userEl.textContent = myUsername;
450
+ if (toolEl) toolEl.textContent = selectedObjectType || "None";
451
  }
452
 
453
+ // --- Animation Loop & Player Movement/Camera ---
454
+ let lastPositionSendTime = 0;
455
+ const positionUpdateInterval = 100; // Send position every 100ms
456
+
457
+ function updatePlayerMovement() {
458
+ if (!playerMesh) return;
459
+ const moveDirection = new THREE.Vector3(0, 0, 0);
460
+ if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
461
+ if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
462
+ if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
463
+ if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
464
+
465
+ let positionChanged = false;
466
+ if (moveDirection.lengthSq() > 0) {
467
+ const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize();
468
+ const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
469
+ const worldMove = new THREE.Vector3();
470
+ worldMove.add(forward.multiplyScalar(-moveDirection.z * playerSpeed));
471
+ worldMove.add(right.multiplyScalar(-moveDirection.x * playerSpeed));
472
+
473
+ playerMesh.position.add(worldMove);
474
+ playerMesh.position.y = Math.max(playerMesh.position.y, 0.8); // Ground clamp
475
+ checkAndExpandGroundVisuals(); // Check if new ground needed
476
+ positionChanged = true;
477
+ }
478
+
479
+ // Send position update periodically if moved
480
+ const now = Date.now();
481
+ if (positionChanged && now - lastPositionSendTime > positionUpdateInterval) {
482
+ sendWebSocketMessage("player_position", {
483
+ position: { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z },
484
+ rotation: { _x: playerMesh.rotation.x, _y: playerMesh.rotation.y, _z: playerMesh.rotation.z, _order: playerMesh.rotation.order }
485
+ });
486
+ lastPositionSendTime = now;
487
+ }
488
  }
489
+ function updateCamera() {
490
+ if (!playerMesh) return;
491
+ const offset = new THREE.Vector3(0, 12, 18);
492
+ const targetPosition = playerMesh.position.clone().add(offset);
493
+ camera.position.lerp(targetPosition, 0.08);
494
+ camera.lookAt(playerMesh.position);
495
  }
496
 
497
  function animate() {
498
  requestAnimationFrame(animate);
499
+ updatePlayerMovement(); // Includes ground check & position sending
500
  updateCamera();
501
  renderer.render(scene, camera);
502
  }